All Downloads are FREE. Search and download functionalities are using the official Maven repository.

d.core.2024.9.2.source-code.JavaMutation.kt Maven / Gradle / Ivy

The newest version!
@file:Suppress("MatchingDeclarationName", "ktlint:standard:filename")

package edu.illinois.cs.cs125.jeed.core

import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.BlockContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.BlockStatementContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.ExpressionContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.LiteralContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.PrimaryContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.StatementContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.SwitchBlockStatementGroupContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParser.SwitchRuleOutcomeContext
import edu.illinois.cs.cs125.jeed.core.antlr.JavaParserBaseListener
import org.antlr.v4.runtime.ParserRuleContext
import org.antlr.v4.runtime.RuleContext
import org.antlr.v4.runtime.Token
import org.antlr.v4.runtime.tree.ParseTreeWalker
import org.antlr.v4.runtime.tree.TerminalNode
import org.jetbrains.kotlin.backend.common.pop

@Suppress("TooManyFunctions", "ComplexMethod", "LongMethod")
class JavaMutationListener(private val parsedSource: Source.ParsedSource) : JavaParserBaseListener() {
    val lines = parsedSource.contents.lines()
    val mutations: MutableList = mutableListOf()

    private val returnTypeStack: MutableList = mutableListOf()
    private val currentReturnType: String?
        get() = returnTypeStack.lastOrNull()

    override fun enterMethodDeclaration(ctx: JavaParser.MethodDeclarationContext) {
        returnTypeStack.add(ctx.typeTypeOrVoid().text)
    }

    override fun exitMethodDeclaration(ctx: JavaParser.MethodDeclarationContext) {
        check(returnTypeStack.isNotEmpty()) { "Return type stack should not be empty" }
        returnTypeStack.pop()
    }

    override fun enterMethodBody(ctx: JavaParser.MethodBodyContext) {
        if (ctx.block() != null) {
            check(currentReturnType != null)
            val location = ctx.block().toLocation()
            val contents = parsedSource.contents(location)
            if (RemoveMethod.matches(contents, currentReturnType!!, Source.FileType.JAVA)) {
                mutations.add(RemoveMethod(location, contents, currentReturnType!!, Source.FileType.JAVA))
            }
        }
    }

    private var insideAnnotation = false
    override fun enterAnnotation(ctx: JavaParser.AnnotationContext?) {
        insideAnnotation = true
    }

    override fun exitAnnotation(ctx: JavaParser.AnnotationContext?) {
        insideAnnotation = false
    }

    private fun ParserRuleContext.toLocation() =
        Mutation.Location(
            start.startIndex,
            stop.stopIndex,
            lines.filterIndexed { index, _ -> index >= start.line - 1 && index <= stop.line - 1 }
                .joinToString("\n"),
            start.line,
            stop.line,
        )

    private fun Token.toLocation() = Mutation.Location(startIndex, stopIndex, lines[line - 1], line, line)
    private fun List.toLocation() =
        Mutation.Location(
            first().symbol.startIndex,
            last().symbol.stopIndex,
            lines.filterIndexed { index, _ -> index >= first().symbol.line - 1 && index <= last().symbol.line - 1 }
                .joinToString("\n"),
            first().symbol.line,
            last().symbol.line,
        )

    private val divisions = listOf("%", "/", "%=", "/=")
    private fun LiteralContext.checkDivision(): Boolean {
        var current: RuleContext? = this
        while (current != null) {
            if (current is ExpressionContext && divisions.contains(current.bop?.text)) {
                return current.expression(1)?.text?.trimParentheses() == text
            }
            current = current.parent
        }
        return false
    }

    override fun enterLiteral(ctx: LiteralContext) {
        if (insideAnnotation) {
            return
        }
        ctx.BOOL_LITERAL()?.also {
            ctx.toLocation().also { location ->
                mutations.add(BooleanLiteral(location, parsedSource.contents(location), Source.FileType.JAVA))
            }
        }
        ctx.CHAR_LITERAL()?.also {
            ctx.toLocation().also { location ->
                mutations.add(CharLiteral(location, parsedSource.contents(location), Source.FileType.JAVA))
            }
        }
        ctx.STRING_LITERAL()?.also {
            ctx.toLocation().also { location ->
                val contents = parsedSource.contents(location)
                mutations.addStringMutations(location, contents, Source.FileType.JAVA)
            }
        }

        val isNegative = try {
            (((ctx.parent as PrimaryContext).parent as ExpressionContext).parent as ExpressionContext).prefix.text == "-"
        } catch (e: Exception) {
            false
        }
        val isDivision = try {
            ctx.checkDivision()
        } catch (e: Exception) {
            false
        }

        ctx.integerLiteral()?.also { integerLiteral ->
            integerLiteral.DECIMAL_LITERAL()?.also {
                ctx.toLocation().also { location ->
                    val contents = parsedSource.contents(location)
                    mutations.add(NumberLiteral(location, contents, Source.FileType.JAVA, isNegative, isDivision))
                    if (NumberLiteralTrim.matches(
                            contents,
                            base = 10,
                            isNegative = isNegative,
                            isDivision = isDivision,
                        )
                    ) {
                        mutations.add(
                            NumberLiteralTrim(
                                location,
                                contents,
                                Source.FileType.JAVA,
                                base = 10,
                                isNegative = isNegative,
                                isDivision = isDivision,
                            ),
                        )
                    }
                }
            }
            integerLiteral.BINARY_LITERAL()?.also {
                ctx.toLocation().also { location ->
                    val contents = parsedSource.contents(location)
                    mutations.add(NumberLiteral(location, contents, Source.FileType.JAVA, isNegative, isDivision, 2))
                    if (NumberLiteralTrim.matches(contents, 2, isNegative, isDivision)) {
                        mutations.add(
                            NumberLiteralTrim(
                                location,
                                contents,
                                Source.FileType.JAVA,
                                base = 2,
                                isNegative = isNegative,
                                isDivision = isDivision,
                            ),
                        )
                    }
                }
            }
            integerLiteral.HEX_LITERAL()?.also {
                ctx.toLocation().also { location ->
                    val contents = parsedSource.contents(location)
                    mutations.add(NumberLiteral(location, contents, Source.FileType.JAVA, isNegative, isDivision, 16))
                    if (NumberLiteralTrim.matches(contents, 16, isNegative, isDivision)) {
                        mutations.add(
                            NumberLiteralTrim(
                                location,
                                contents,
                                Source.FileType.JAVA,
                                base = 16,
                                isNegative = isNegative,
                                isDivision = isDivision,
                            ),
                        )
                    }
                }
            }
        }
        ctx.floatLiteral()?.also { floatLiteral ->
            floatLiteral.FLOAT_LITERAL()?.also {
                ctx.toLocation().also { location ->
                    val contents = parsedSource.contents(location)
                    mutations.add(NumberLiteral(location, contents, Source.FileType.JAVA, isNegative = isNegative))
                    if (NumberLiteralTrim.matches(contents, base = 10, isNegative = isNegative, isDivision)) {
                        mutations.add(
                            NumberLiteralTrim(
                                location,
                                contents,
                                Source.FileType.JAVA,
                                base = 10,
                                isNegative = isNegative,
                                isDivision = isDivision,
                            ),
                        )
                    }
                }
            }
        }
    }

    private fun ExpressionContext.locationPair(): Pair {
        check(expression().size == 2)
        val front = expression(0)
        val back = expression(1)
        val frontLocation = Mutation.Location(
            front.start.startIndex,
            back.start.startIndex - 1,
            lines
                .filterIndexed { index, _ ->
                    index >= front.start.line - 1 && index <= back.start.line - 1
                }
                .joinToString("\n"),
            front.start.line,
            back.start.line,
        )
        val backLocation = Mutation.Location(
            front.stop.stopIndex + 1,
            back.stop.stopIndex,
            lines
                .filterIndexed { index, _ ->
                    index >= front.stop.line - 1 && index <= back.stop.line - 1
                }
                .joinToString("\n"),
            front.start.line,
            back.stop.line,
        )
        return Pair(frontLocation, backLocation)
    }

    private var insideLambda = false
    override fun enterExpression(ctx: ExpressionContext) {
        ctx.creator()?.classCreatorRest()?.classBody()?.also {
            insideLambda = true
        }
        ctx.lambdaExpression()?.also {
            insideLambda = true
        }
        ctx.prefix?.toLocation()?.also { location ->
            val contents = parsedSource.contents(location)
            if (IncrementDecrement.matches(contents)) {
                mutations.add(IncrementDecrement(location, contents, Source.FileType.JAVA))
            }
            if (InvertNegation.matches(contents)) {
                mutations.add(InvertNegation(location, contents, Source.FileType.JAVA))
            }
        }
        ctx.postfix?.toLocation()?.also { location ->
            val contents = parsedSource.contents(location)
            if (IncrementDecrement.matches(contents)) {
                mutations.add(IncrementDecrement(location, contents, Source.FileType.JAVA))
            }
        }

        ctx.LT()?.also { tokens ->
            if (tokens.size == 2) {
                tokens.toLocation().also { location ->
                    val contents = parsedSource.contents(location)
                    if (MutateMath.matches(contents)) {
                        mutations.add(MutateMath(location, contents, Source.FileType.JAVA))
                    }
                    if (RemoveBinary.matches(contents)) {
                        val (frontLocation, backLocation) = ctx.locationPair()
                        mutations.add(
                            RemoveBinary(
                                frontLocation,
                                parsedSource.contents(frontLocation),
                                Source.FileType.JAVA,
                            ),
                        )
                        mutations.add(
                            RemoveBinary(
                                backLocation,
                                parsedSource.contents(backLocation),
                                Source.FileType.JAVA,
                            ),
                        )
                    }
                }
            }
        }

        // I'm not sure why you can't write this like the other ones, but it fails with a cast to kotlin.Unit
        // exception
        @Suppress("MagicNumber")
        if (ctx.GT() != null && (ctx.GT().size == 2 || ctx.GT().size == 3)) {
            val location = ctx.GT().toLocation()
            val contents = parsedSource.contents(location)
            if (MutateMath.matches(contents)) {
                mutations.add(MutateMath(location, contents, Source.FileType.JAVA))
            }
            if (RemoveBinary.matches(contents)) {
                val (frontLocation, backLocation) = ctx.locationPair()
                mutations.add(RemoveBinary(frontLocation, parsedSource.contents(frontLocation), Source.FileType.JAVA))
                mutations.add(RemoveBinary(backLocation, parsedSource.contents(backLocation), Source.FileType.JAVA))
            }
        }

        ctx.bop?.toLocation()?.also { location ->
            val contents = parsedSource.contents(location)
            if (ConditionalBoundary.matches(contents)) {
                mutations.add(ConditionalBoundary(location, contents, Source.FileType.JAVA))
            }
            if (NegateConditional.matches(contents)) {
                mutations.add(NegateConditional(location, contents, Source.FileType.JAVA))
            }
            if (MutateMath.matches(contents)) {
                mutations.add(MutateMath(location, contents, Source.FileType.JAVA))
            }
            if (PlusToMinus.matches(contents)) {
                mutations.add(PlusToMinus(location, contents, Source.FileType.JAVA))
            }
            if (SwapAndOr.matches(contents)) {
                mutations.add(SwapAndOr(location, contents, Source.FileType.JAVA))
            }
            @Suppress("ComplexCondition")
            if (contents == "&&" || contents == "||") {
                val (frontLocation, backLocation) = ctx.locationPair()
                mutations.add(RemoveAndOr(frontLocation, parsedSource.contents(frontLocation), Source.FileType.JAVA))
                mutations.add(RemoveAndOr(backLocation, parsedSource.contents(backLocation), Source.FileType.JAVA))
            }
            if (RemovePlus.matches(contents)) {
                val (frontLocation, backLocation) = ctx.locationPair()
                mutations.add(RemovePlus(frontLocation, parsedSource.contents(frontLocation), Source.FileType.JAVA))
                mutations.add(RemovePlus(backLocation, parsedSource.contents(backLocation), Source.FileType.JAVA))
            }
            if (RemoveBinary.matches(contents)) {
                val (frontLocation, backLocation) = ctx.locationPair()
                mutations.add(RemoveBinary(frontLocation, parsedSource.contents(frontLocation), Source.FileType.JAVA))
                mutations.add(RemoveBinary(backLocation, parsedSource.contents(backLocation), Source.FileType.JAVA))
            }
            if (contents == "==") {
                mutations.add(
                    ChangeEquals(
                        ctx.toLocation(),
                        parsedSource.contents(ctx.toLocation()),
                        Source.FileType.JAVA,
                        "==",
                        parsedSource.contents(ctx.expression(0).toLocation()),
                        parsedSource.contents(ctx.expression(1).toLocation()),
                    ),
                )
            }
            @Suppress("ComplexCondition")
            if (contents == "." &&
                ctx.methodCall()?.identifier()?.text == "equals" &&
                ctx.methodCall()?.arguments()?.expressionList()?.expression()?.size == 1
            ) {
                mutations.add(
                    ChangeEquals(
                        ctx.toLocation(),
                        parsedSource.contents(ctx.toLocation()),
                        Source.FileType.JAVA,
                        ".equals",
                        parsedSource.contents(ctx.expression(0).toLocation()),
                        parsedSource.contents(ctx.methodCall().arguments().expressionList().expression(0).toLocation()),
                    ),
                )
            }
            if (contents == "." && ctx.identifier()?.text == "length") {
                mutations.add(
                    ChangeLengthAndSize(
                        ctx.identifier().toLocation(),
                        parsedSource.contents(ctx.identifier().toLocation()),
                        Source.FileType.JAVA,
                    ),
                )
            }
            if (contents == "." &&
                (ctx.methodCall()?.identifier()?.text == "length" || ctx.methodCall()?.identifier()?.text == "size") &&
                ctx.methodCall()?.arguments()?.expressionList()?.isEmpty != false
            ) {
                mutations.add(
                    ChangeLengthAndSize(
                        ctx.methodCall().toLocation(),
                        parsedSource.contents(ctx.methodCall().toLocation()),
                        Source.FileType.JAVA,
                    ),
                )
            }
            if (contents == "+" || contents == "-") {
                val text = parsedSource.contents(ctx.expression(1).toLocation())
                if (text == "1") {
                    mutations.add(
                        PlusOrMinusOneToZero(
                            ctx.expression(1).toLocation(),
                            parsedSource.contents(ctx.expression(1).toLocation()),
                            Source.FileType.JAVA,
                        ),
                    )
                }
            }
        }
    }

    override fun exitExpression(ctx: ExpressionContext) {
        ctx.creator()?.classCreatorRest()?.classBody()?.also {
            insideLambda = false
        }
        ctx.lambdaExpression()?.also {
            insideLambda = false
        }
    }

    private val seenIfStarts = mutableSetOf()

    private var loopDepth = 0
    private var loopBlockDepths = mutableListOf()
    private var continueUntil = 0

    @Suppress("unused")
    private fun StatementContext.hasElse(): Boolean {
        check(IF() != null)
        var currentIf = this
        while (true) {
            if (currentIf.IF() == null) {
                check(currentIf.block() != null)
                return true
            }
            if (currentIf.ELSE() == null || currentIf.statement(1) == null) {
                return false
            }
            currentIf = currentIf.statement(1)
        }
    }

    private fun BlockContext.setContinueUntil() {
        continueUntil = 0
        if (blockStatement().isEmpty()) {
            return
        }
        var sawIf = false
        blockStatement().reversed().find {
            val result = if (it.localVariableDeclaration() != null || it.localTypeDeclaration() != null) {
                true
            } else if (sawIf && (it.statement().IF() != null || it.statement().TRY() != null)) {
                true
            } else {
                it.statement().CONTINUE() == null && it.statement().IF() == null && it.statement().TRY() == null
            }
            if (it.statement()?.IF() != null || it.statement()?.TRY() != null) {
                sawIf = true
            }
            result
        }?.also {
            continueUntil = it.toLocation().end
        }
    }

    override fun enterStatement(ctx: StatementContext) {
        ctx.IF()?.also {
            val outerLocation = ctx.toLocation()
            if (outerLocation.start !in seenIfStarts) {
                // Add entire if
                mutations.add(RemoveIf(outerLocation, parsedSource.contents(outerLocation), Source.FileType.JAVA))
                seenIfStarts += outerLocation.start
                check(ctx.statement().isNotEmpty())
                if (ctx.statement().size == 2 && ctx.statement(1).block() != null) {
                    // Add else branch (2)
                    check(ctx.ELSE() != null)
                    val start = ctx.ELSE().symbol
                    val end = ctx.statement(1).block().stop
                    val elseLocation =
                        Mutation.Location(
                            start.startIndex,
                            end.stopIndex,
                            lines.filterIndexed { index, _ -> index >= start.line - 1 && index <= end.line - 1 }
                                .joinToString("\n"),
                            start.line,
                            end.line,
                        )
                    mutations.add(RemoveIf(elseLocation, parsedSource.contents(elseLocation), Source.FileType.JAVA))
                } else if (ctx.statement().size >= 2) {
                    var statement = ctx.statement(1)
                    var previousMarker = ctx.ELSE()
                    check(previousMarker != null)
                    while (statement != null) {
                        if (statement.IF() != null) {
                            seenIfStarts += statement.toLocation().start
                        }
                        val end = statement.statement(0) ?: statement.block()
                        if (end != null) {
                            val currentLocation =
                                Mutation.Location(
                                    previousMarker.symbol.startIndex,
                                    end.stop.stopIndex,
                                    lines
                                        .filterIndexed { index, _ ->
                                            index >= previousMarker.symbol.line - 1 && index <= end.stop.line - 1
                                        }
                                        .joinToString("\n"),
                                    previousMarker.symbol.line,
                                    end.stop.line,
                                )
                            mutations.add(
                                RemoveIf(
                                    currentLocation,
                                    parsedSource.contents(currentLocation),
                                    Source.FileType.JAVA,
                                ),
                            )
                        }
                        previousMarker = statement.ELSE()
                        statement = statement.statement(1)
                    }
                }
            }
            ctx.parExpression().toLocation().also { location ->
                mutations.add(NegateIf(location, parsedSource.contents(location), Source.FileType.JAVA))
            }
        }
        ctx.ASSERT()?.also {
            ctx.toLocation().also { location ->
                mutations.add(RemoveRuntimeCheck(location, parsedSource.contents(location), Source.FileType.JAVA))
            }
        }
        ctx.RETURN()?.also {
            ctx.expression()?.firstOrNull()?.toLocation()?.also { location ->
                val contents = parsedSource.contents(location)
                currentReturnType?.also { returnType ->
                    if (PrimitiveReturn.matches(contents, returnType, Source.FileType.JAVA)) {
                        mutations.add(PrimitiveReturn(location, parsedSource.contents(location), Source.FileType.JAVA))
                    }
                    if (TrueReturn.matches(contents, returnType)) {
                        mutations.add(TrueReturn(location, parsedSource.contents(location), Source.FileType.JAVA))
                    }
                    if (FalseReturn.matches(contents, returnType)) {
                        mutations.add(FalseReturn(location, parsedSource.contents(location), Source.FileType.JAVA))
                    }
                    if (NullReturn.matches(contents, returnType, Source.FileType.JAVA)) {
                        mutations.add(NullReturn(location, parsedSource.contents(location), Source.FileType.JAVA))
                    }
                } ?: error("Should have recorded a return type at this point")
            }
        }
        ctx.WHILE()?.also {
            loopDepth++
            loopBlockDepths.add(0, 0)
            ctx.statement(0).block()?.setContinueUntil()
            ctx.parExpression().toLocation().also { location ->
                mutations.add(NegateWhile(location, parsedSource.contents(location), Source.FileType.JAVA))
            }
            if (ctx.DO() == null) {
                mutations.add(
                    RemoveLoop(
                        ctx.toLocation(),
                        parsedSource.contents(ctx.toLocation()),
                        Source.FileType.JAVA,
                    ),
                )
            }
        }
        ctx.FOR()?.also {
            loopDepth++
            loopBlockDepths.add(0, 0)
            ctx.statement(0).block()?.setContinueUntil()
            mutations.add(RemoveLoop(ctx.toLocation(), parsedSource.contents(ctx.toLocation()), Source.FileType.JAVA))
        }
        ctx.DO()?.also {
            mutations.add(RemoveLoop(ctx.toLocation(), parsedSource.contents(ctx.toLocation()), Source.FileType.JAVA))
        }
        ctx.TRY()?.also {
            mutations.add(RemoveTry(ctx.toLocation(), parsedSource.contents(ctx.toLocation()), Source.FileType.JAVA))
        }
        ctx.statementExpression?.also {
            val inSwitch = try {
                val blockStatement = (ctx.parent as BlockStatementContext).parent
                blockStatement is SwitchBlockStatementGroupContext || blockStatement is SwitchRuleOutcomeContext
            } catch (e: Exception) {
                false
            }
            if (!inSwitch) {
                mutations.add(
                    RemoveStatement(
                        ctx.toLocation(),
                        parsedSource.contents(ctx.toLocation()),
                        Source.FileType.JAVA,
                    ),
                )
            }
        }
        ctx.BREAK()?.symbol?.also {
            mutations.add(
                SwapBreakContinue(
                    it.toLocation(),
                    parsedSource.contents(it.toLocation()),
                    Source.FileType.JAVA,
                ),
            )
        }
        ctx.CONTINUE()?.symbol?.also {
            mutations.add(
                SwapBreakContinue(
                    it.toLocation(),
                    parsedSource.contents(it.toLocation()),
                    Source.FileType.JAVA,
                ),
            )
        }
    }

    override fun enterBlock(ctx: BlockContext?) {
        if (loopDepth > 0) {
            loopBlockDepths[0]++
        }
    }

    override fun exitBlock(ctx: BlockContext) {
        if (loopDepth > 0 && !insideLambda) {
            val rbrace = ctx.RBRACE()
            val endBraceLocation = listOf(rbrace, rbrace).toLocation()
            val location = rbrace.symbol.toLocation()
            check(location.startLine == location.endLine)
            val previousLine = lines[location.startLine - 2].trim()
            if (previousLine != "break;") {
                mutations.add(
                    AddBreak(
                        endBraceLocation,
                        parsedSource.contents(endBraceLocation),
                        Source.FileType.JAVA,
                    ),
                )
            }
            if (loopBlockDepths[0] > 1 && previousLine != "continue;" && location.start <= continueUntil) {
                mutations.add(
                    AddContinue(
                        endBraceLocation,
                        parsedSource.contents(endBraceLocation),
                        Source.FileType.JAVA,
                    ),
                )
            }
        }
        if (loopDepth > 0) {
            loopBlockDepths[0]--
        }
    }

    override fun exitStatement(ctx: StatementContext) {
        (ctx.WHILE() ?: ctx.DO() ?: ctx.FOR())?.also {
            loopDepth--
            check(loopBlockDepths.removeAt(0) == 0)
        }
    }

    override fun enterArrayInitializer(ctx: JavaParser.ArrayInitializerContext) {
        if ((ctx.variableInitializer()?.size ?: 0) < 2) {
            return
        }
        val start = ctx.variableInitializer().first().toLocation()
        val end = ctx.variableInitializer().last().toLocation()
        val location = Mutation.Location(
            start.start,
            end.end,
            lines.filterIndexed { index, _ -> index >= start.startLine - 1 && index <= end.endLine - 1 }
                .joinToString("\n"),
            start.startLine,
            end.endLine,
        )
        val contents = parsedSource.contents(location)
        val parts = ctx.variableInitializer().map {
            parsedSource.contents(it.toLocation())
        }
        mutations.add(ModifyArrayLiteral(location, contents, Source.FileType.JAVA, parts))
    }

    init {
        // println(parsedSource.tree.format(parsedSource.parser))
        ParseTreeWalker.DEFAULT.walk(this, parsedSource.tree)
        check(loopDepth == 0)
        check(loopBlockDepths.size == 0)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy