d.core.2024.9.2.source-code.KotlinFeatures.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", "ktlint:standard:filename")
package edu.illinois.cs.cs125.jeed.core
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.AnonymousInitializerContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.ClassBodyContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.ControlStructureBodyContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.EmptyFunctionDeclarationContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.FunctionBodyContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.FunctionDeclarationContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.PropertyDeclarationContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.SecondaryConstructorContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParser.StatementContext
import edu.illinois.cs.cs125.jeed.core.antlr.KotlinParserBaseListener
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
private val basicTypes = setOf("Byte", "Short", "Int", "Long", "Float", "Double", "Char", "Boolean")
private val typeCasts = (basicTypes - setOf("Boolean")).map { "to$it" }.toSet()
internal val seenKotlinFeatures = mutableSetOf()
internal var watchKotlinFeatures = false
@Suppress("TooManyFunctions", "LargeClass", "MagicNumber", "LongMethod", "ComplexMethod")
class KotlinFeatureListener(val source: Source, entry: Map.Entry) : KotlinParserBaseListener() {
private val parsedSource: Source.ParsedSource
@Suppress("unused")
private val contents = entry.value
private val filename = entry.key
private var anonymousClassDepth = 0
private var objectLiteralCounter = 0
private var featureStack: MutableList = mutableListOf()
private val currentFeatures: FeatureValue
get() = featureStack[0]
var results: MutableMap = mutableMapOf()
private val currentFeatureMap: MutableMap
get() = currentFeatures.features.featureMap
private val currentFeatureList: MutableList
get() = currentFeatures.features.featureList
private fun count(feature: FeatureName, location: Location) {
val remappedLocation = try {
source.mapLocation(filename, location)
} catch (_: SourceMappingException) {
return
}
currentFeatureMap[feature] = (currentFeatureMap[feature] ?: 0) + 1
currentFeatureList += LocatedFeature(feature, remappedLocation)
if (watchKotlinFeatures) {
seenKotlinFeatures += feature
}
}
private fun add(feature: FeatureName, location: Location) {
val remappedLocation = try {
source.mapLocation(filename, location)
} catch (_: SourceMappingException) {
return
}
currentFeatureList += LocatedFeature(feature, remappedLocation)
}
private fun ParserRuleContext.toLocation() = Location(start.line, start.charPositionInLine)
private fun TerminalNode.toLocation() = Location(symbol.line, symbol.charPositionInLine)
private fun Token.toLocation() = Location(line, charPositionInLine)
private fun Token.previousToken(): String {
for (i in (tokenIndex - 1) downTo 0) {
parsedSource.tokenStream.get(i).text.trim().also {
if (it != "") {
return it
}
}
}
return ""
}
override fun enterKotlinFile(ctx: KotlinParser.KotlinFileContext) {
val unitFeatures = UnitFeatures(
filename,
SourceRange(
filename,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
),
)
assert(featureStack.isEmpty())
featureStack.add(0, unitFeatures)
}
override fun exitKotlinFile(ctx: KotlinParser.KotlinFileContext?) {
assert(featureStack.size == 1)
val topLevelFeatures = featureStack.removeAt(0).finalize() as UnitFeatures
assert(!results.keys.contains(topLevelFeatures.name))
results[topLevelFeatures.name] = topLevelFeatures
}
private fun enterClassOrInterface(name: String, start: Location, end: Location) {
val locatedClass = if (source is Snippet && name == source.wrappedClassName) {
ClassFeatures("", source.snippetRange)
} else {
val range = try {
SourceRange(filename, source.mapLocation(filename, start), source.mapLocation(filename, end))
} catch (e: SourceMappingException) {
null
}
ClassFeatures(name, range)
}
if (featureStack.isNotEmpty()) {
assert(!currentFeatures.classes.containsKey(locatedClass.name))
currentFeatures.classes[locatedClass.name] = locatedClass
}
initCounter = 0
featureStack.add(0, locatedClass)
}
private fun exitClassOrInterface(name: String, start: Location, end: Location) {
assert(featureStack.isNotEmpty())
val lastFeatures = featureStack.removeAt(0).finalize()
assert(lastFeatures is ClassFeatures)
lastFeatures.methods["init0"]?.also {
require(!currentFeatures.methods.containsKey("init"))
lastFeatures.methods["init"] = lastFeatures.methods
.filterKeys { it.startsWith("init") }
.values.map { (it as MethodFeatures).features }
.reduce { first, second ->
first + second
}.let { features ->
val range = try {
SourceRange(filename, source.mapLocation(filename, start), source.mapLocation(filename, end))
} catch (e: SourceMappingException) {
null
}
MethodFeatures(name, range, features = features)
}
}
if (featureStack.isNotEmpty()) {
currentFeatures.features += lastFeatures.features
}
}
private fun KotlinParser.ClassDeclarationContext.isSnippetClass() = source is Snippet &&
simpleIdentifier().text == source.wrappedClassName
private fun FunctionDeclarationContext.isSnippetMethod() = source is Snippet &&
fullName() == source.looseCodeMethodName
private fun FunctionDeclarationContext.fullName(): String {
val name = simpleIdentifier().text
val parameters = functionValueParameters().functionValueParameter()?.joinToString(",") {
it.parameter().type().text
}
val returnType = type()?.text
return ("$name($parameters)${returnType?.let { ":$returnType" } ?: ""}").let {
if (anonymousClassDepth > 0) {
"${it}${"$"}$objectLiteralCounter"
} else {
it
}
}
}
private fun EmptyFunctionDeclarationContext.fullName(): String {
val name = simpleIdentifier().text
val parameters = functionValueParameters().functionValueParameter()?.joinToString(",") {
it.parameter().type().text
}
val returnType = type()?.text
return ("$name($parameters)${returnType?.let { ":$returnType" } ?: ""}").let {
if (anonymousClassDepth > 0) {
"${it}${"$"}$objectLiteralCounter"
} else {
it
}
}
}
override fun enterClassDeclaration(ctx: KotlinParser.ClassDeclarationContext) {
if (!ctx.isSnippetClass()) {
count(FeatureName.CLASS, ctx.toLocation())
ctx.modifiers()?.modifier(0)?.classModifier()?.DATA()?.also {
count(FeatureName.DATA_CLASS, ctx.toLocation())
}
ctx.modifiers()?.modifier(0)?.inheritanceModifier()?.OPEN()?.also {
count(FeatureName.OPEN_CLASS, ctx.toLocation())
}
ctx.modifiers()?.modifier(0)?.inheritanceModifier()?.ABSTRACT()?.also {
count(FeatureName.ABSTRACT_CLASS, ctx.toLocation())
}
ctx.classBody()?.classMemberDeclarations()?.classMemberDeclaration()?.find { it.companionObject() != null }
?.also {
count(FeatureName.HAS_COMPANION_OBJECT, ctx.toLocation())
}
}
enterClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
ctx.delegationSpecifiers()?.annotatedDelegationSpecifier()?.map { it.delegationSpecifier() }
?.forEach { delegationSpecifier ->
val specifier = delegationSpecifier.text.trim()
if (specifier.contains("(") && specifier.endsWith(")")) {
count(FeatureName.EXTENDS, delegationSpecifier.toLocation())
} else {
count(FeatureName.IMPLEMENTS, delegationSpecifier.toLocation())
}
delegationSpecifier.userType()?.simpleUserType()?.firstOrNull()?.simpleIdentifier()?.also {
if (it.text == "Comparable") {
count(FeatureName.COMPARABLE, it.toLocation())
}
}
}
ctx.typeParameters()?.also {
count(FeatureName.GENERIC_CLASS, ctx.typeParameters().toLocation())
}
}
override fun exitClassDeclaration(ctx: KotlinParser.ClassDeclarationContext) {
exitClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun enterEmptyClassDeclaration(ctx: KotlinParser.EmptyClassDeclarationContext) {
count(FeatureName.CLASS, ctx.toLocation())
ctx.modifiers()?.modifier(0)?.classModifier()?.DATA()?.also {
count(FeatureName.DATA_CLASS, ctx.toLocation())
}
ctx.modifiers()?.modifier(0)?.inheritanceModifier()?.OPEN()?.also {
count(FeatureName.OPEN_CLASS, ctx.toLocation())
}
ctx.modifiers()?.modifier(0)?.inheritanceModifier()?.ABSTRACT()?.also {
count(FeatureName.ABSTRACT_CLASS, ctx.toLocation())
}
enterClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
ctx.delegationSpecifiers()?.annotatedDelegationSpecifier()?.map { it.delegationSpecifier() }
?.forEach { delegationSpecifier ->
val specifier = delegationSpecifier.text.trim()
if (specifier.contains("(") && specifier.endsWith(")")) {
count(FeatureName.EXTENDS, delegationSpecifier.toLocation())
} else {
count(FeatureName.IMPLEMENTS, delegationSpecifier.toLocation())
}
delegationSpecifier.userType()?.simpleUserType()?.firstOrNull()?.simpleIdentifier()?.also {
if (it.text == "Comparable") {
count(FeatureName.COMPARABLE, it.toLocation())
}
}
}
ctx.typeParameters()?.also {
count(FeatureName.GENERIC_CLASS, ctx.typeParameters().toLocation())
}
}
override fun exitEmptyClassDeclaration(ctx: KotlinParser.EmptyClassDeclarationContext) {
exitClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun enterInterfaceDeclaration(ctx: KotlinParser.InterfaceDeclarationContext) {
count(FeatureName.INTERFACE, ctx.toLocation())
ctx.FUN()?.also {
count(FeatureName.FUNCTIONAL_INTERFACE, ctx.toLocation())
}
enterClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun exitInterfaceDeclaration(ctx: KotlinParser.InterfaceDeclarationContext) {
exitClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
private fun enterMethodOrConstructor(name: String, start: Location, end: Location) {
val locatedMethod =
if (source is Snippet &&
source.looseCodeMethodName == name
) {
MethodFeatures("", source.snippetRange)
} else {
val range = try {
SourceRange(filename, source.mapLocation(filename, start), source.mapLocation(filename, end))
} catch (e: SourceMappingException) {
null
}
MethodFeatures(name, range)
}
if (featureStack.isNotEmpty()) {
require(!currentFeatures.methods.containsKey(locatedMethod.name))
currentFeatures.methods[locatedMethod.name] = locatedMethod
}
featureStack.add(0, locatedMethod)
}
private fun exitMethodOrConstructor() {
require(featureStack.isNotEmpty())
check(arrayDepth == 0)
check(arrayTypeDepth == 0)
val lastFeatures = featureStack.removeAt(0).finalize()
require(lastFeatures is MethodFeatures)
assert(featureStack.isNotEmpty())
currentFeatures.features += lastFeatures.features
}
private var functionBlockDepths = mutableListOf()
@Suppress("unused")
private val currentBlockDepth
get() = functionBlockDepths.last()
private var ifDepths = mutableListOf()
private val ifDepth
get() = ifDepths.last()
private var loopDepths = mutableListOf()
private val loopDepth
get() = loopDepths.last()
override fun enterFunctionDeclaration(ctx: FunctionDeclarationContext) {
if (!ctx.isSnippetMethod()) {
count(FeatureName.METHOD, ctx.toLocation())
}
enterMethodOrConstructor(
ctx.fullName(),
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
functionBlockDepths += 0
ifDepths += 0
loopDepths += 0
if (ctx.parentType() == ParentType.FUNCTION) {
count(FeatureName.NESTED_METHOD, ctx.toLocation())
}
ctx.modifiers()?.modifier()?.find { it.memberModifier()?.OVERRIDE() != null }?.also { modifier ->
count(FeatureName.OVERRIDE, modifier.toLocation())
}
ctx.modifiers()?.modifier()?.find { it.inheritanceModifier()?.OPEN() != null }?.also { modifier ->
count(FeatureName.OPEN_METHOD, modifier.toLocation())
}
}
override fun enterEmptyFunctionDeclaration(ctx: EmptyFunctionDeclarationContext) {
if (ctx.modifiers()?.modifier()?.any { it.inheritanceModifier()?.ABSTRACT() != null } == true) {
count(FeatureName.ABSTRACT_METHOD, ctx.toLocation())
} else {
count(FeatureName.METHOD, ctx.toLocation())
}
enterMethodOrConstructor(
ctx.fullName(),
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun exitEmptyFunctionDeclaration(ctx: EmptyFunctionDeclarationContext?) {
exitMethodOrConstructor()
}
override fun exitFunctionDeclaration(ctx: FunctionDeclarationContext) {
exitMethodOrConstructor()
val exitingBlockDepth = functionBlockDepths.pop()
check(exitingBlockDepth == 0)
val exitingIfDepth = ifDepths.pop()
check(exitingIfDepth == 0)
val exitingLoopDepth = loopDepths.pop()
check(exitingLoopDepth == 0)
}
private var initCounter = 0
override fun enterAnonymousInitializer(ctx: AnonymousInitializerContext) {
ifDepths += 0
functionBlockDepths += 0
loopDepths += 0
enterMethodOrConstructor(
"init${initCounter++}",
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun exitAnonymousInitializer(ctx: AnonymousInitializerContext?) {
exitMethodOrConstructor()
val exitingBlockDepth = functionBlockDepths.pop()
check(exitingBlockDepth == 0)
val exitingIfDepth = ifDepths.pop()
check(exitingIfDepth == 0)
val exitingLoopDepth = loopDepths.pop()
check(exitingLoopDepth == 0)
}
private var constructorCounter = 0
override fun enterSecondaryConstructor(ctx: SecondaryConstructorContext) {
count(FeatureName.CONSTRUCTOR, ctx.toLocation())
count(FeatureName.SECONDARY_CONSTRUCTOR, ctx.toLocation())
ifDepths += 0
functionBlockDepths += 0
loopDepths += 0
enterMethodOrConstructor(
"constructor${constructorCounter++}",
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun exitSecondaryConstructor(ctx: SecondaryConstructorContext) {
exitMethodOrConstructor()
val exitingBlockDepth = functionBlockDepths.pop()
check(exitingBlockDepth == 0)
val exitingIfDepth = ifDepths.pop()
check(exitingIfDepth == 0)
val exitingLoopDepth = loopDepths.pop()
check(exitingLoopDepth == 0)
}
override fun enterBlock(ctx: KotlinParser.BlockContext) {
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
if (functionBlockDepths.isNotEmpty()) {
functionBlockDepths[functionBlockDepths.size - 1]++
}
}
override fun exitBlock(ctx: KotlinParser.BlockContext?) {
if (functionBlockDepths.isNotEmpty()) {
functionBlockDepths[functionBlockDepths.size - 1]--
}
}
private enum class ParentType {
FUNCTION,
CLASS,
NONE,
}
private fun ParserRuleContext.parentContext(): RuleContext? {
var currentParent = parent
while (currentParent != null) {
when (currentParent) {
is FunctionBodyContext -> return currentParent
is ClassBodyContext -> return currentParent
is AnonymousInitializerContext -> return currentParent
is SecondaryConstructorContext -> return currentParent
}
currentParent = currentParent.parent
}
return null
}
@Suppress("unused")
private inline fun ParserRuleContext.searchUp(): T? {
var currentParent = parent
while (currentParent != null) {
currentParent = currentParent.parent
if (currentParent is T) {
return currentParent
}
}
return null
}
private fun ParserRuleContext.parentType() = when (parentContext()) {
is FunctionBodyContext -> ParentType.FUNCTION
is AnonymousInitializerContext -> ParentType.FUNCTION
is SecondaryConstructorContext -> ParentType.FUNCTION
is ClassBodyContext -> ParentType.CLASS
else -> ParentType.NONE
}
override fun enterVariableDeclaration(ctx: KotlinParser.VariableDeclarationContext) {
if (ctx.parent is PropertyDeclarationContext && ctx.parentType() == ParentType.FUNCTION) {
count(FeatureName.LOCAL_VARIABLE_DECLARATIONS, ctx.toLocation())
count(FeatureName.VARIABLE_ASSIGNMENTS, ctx.toLocation())
(ctx.parent as PropertyDeclarationContext).VAL()?.also {
count(FeatureName.FINAL_VARIABLE, it.toLocation())
}
}
val type = ctx.type()?.text?.trim()
if (type != null) {
count(FeatureName.EXPLICIT_TYPE, ctx.type().toLocation())
val baseType = type.removeSuffix("?")
if (baseType == "String") {
count(FeatureName.STRING, ctx.type().toLocation())
}
if (baseType != type) {
count(FeatureName.NULLABLE_TYPE, ctx.type().toLocation())
}
} else {
count(FeatureName.TYPE_INFERENCE, ctx.toLocation())
}
}
override fun enterAssignment(ctx: KotlinParser.AssignmentContext) {
if (ctx.parentType() == ParentType.FUNCTION) {
count(FeatureName.VARIABLE_REASSIGNMENTS, ctx.toLocation())
}
}
override fun enterPrefixUnaryOperator(ctx: KotlinParser.PrefixUnaryOperatorContext) {
if (ctx.parentType() == ParentType.FUNCTION) {
if (ctx.INCR() != null || ctx.DECR() != null) {
count(FeatureName.VARIABLE_REASSIGNMENTS, (ctx.INCR() ?: ctx.DECR()).toLocation())
count(FeatureName.UNARY_OPERATORS, (ctx.INCR() ?: ctx.DECR()).toLocation())
} else if (ctx.EXCL_WS() != null || ctx.EXCL_NO_WS() != null) {
val location = ctx.EXCL_WS()?.toLocation() ?: ctx.EXCL_NO_WS()?.toLocation()
count(FeatureName.LOGICAL_OPERATORS, location!!)
}
}
}
override fun enterPostfixUnaryOperator(ctx: KotlinParser.PostfixUnaryOperatorContext) {
if (ctx.parentType() == ParentType.FUNCTION) {
if (ctx.INCR() != null || ctx.DECR() != null) {
count(FeatureName.VARIABLE_REASSIGNMENTS, (ctx.INCR() ?: ctx.DECR())!!.toLocation())
count(FeatureName.UNARY_OPERATORS, (ctx.INCR() ?: ctx.DECR())!!.toLocation())
}
}
}
override fun enterLoopStatement(ctx: KotlinParser.LoopStatementContext) {
if (ctx.parentType() != ParentType.FUNCTION) {
return
}
ctx.forStatement()?.also {
count(FeatureName.FOR_LOOPS, ctx.forStatement().toLocation())
if (loopDepth > 0) {
count(FeatureName.NESTED_FOR, ctx.forStatement().toLocation())
count(FeatureName.NESTED_LOOP, ctx.forStatement().toLocation())
}
}
ctx.whileStatement()?.also {
count(FeatureName.WHILE_LOOPS, ctx.whileStatement().toLocation())
if (loopDepth > 0) {
count(FeatureName.NESTED_WHILE, ctx.whileStatement().toLocation())
count(FeatureName.NESTED_LOOP, ctx.whileStatement().toLocation())
}
}
ctx.doWhileStatement()?.also {
count(FeatureName.DO_WHILE_LOOPS, ctx.doWhileStatement().toLocation())
if (loopDepth > 0) {
count(FeatureName.NESTED_DO_WHILE, ctx.doWhileStatement().toLocation())
count(FeatureName.NESTED_LOOP, ctx.doWhileStatement().toLocation())
}
}
loopDepths[loopDepths.size - 1]++
}
override fun exitLoopStatement(ctx: KotlinParser.LoopStatementContext?) {
loopDepths[loopDepths.size - 1]--
check(loopDepth >= 0)
}
private var inForStatement = false
override fun enterForStatement(ctx: KotlinParser.ForStatementContext?) {
inForStatement = true
}
override fun exitForStatement(ctx: KotlinParser.ForStatementContext?) {
inForStatement = false
}
private val bitwiseOperators = listOf("and", "or", "shl", "shr")
override fun enterSimpleIdentifier(ctx: KotlinParser.SimpleIdentifierContext) {
val identifier = ctx.Identifier()?.text
if (inForStatement && identifier == "step") {
count(FeatureName.FOR_LOOP_STEP, ctx.toLocation())
} else if (bitwiseOperators.contains(identifier)) {
count(FeatureName.BITWISE_OPERATORS, ctx.toLocation())
}
}
// Gotta love this grammar
private fun ControlStructureBodyContext.isIf() = statement()
?.expression()
?.primaryExpression()
?.ifExpression() != null
private val topIfs = mutableSetOf()
private val seenIfStarts = mutableSetOf()
override fun enterIfExpression(ctx: KotlinParser.IfExpressionContext) {
val ifStart = ctx.start.startIndex
if (ifStart !in seenIfStarts) {
count(FeatureName.IF_STATEMENTS, ctx.toLocation())
if (ctx.start.previousToken() == "=") {
count(FeatureName.IF_EXPRESSIONS, ctx.toLocation())
}
seenIfStarts += ifStart
topIfs += ifStart
if (ifDepth > 0) {
count(FeatureName.NESTED_IF, ctx.toLocation())
}
ifDepths[ifDepths.size - 1]++
}
ctx.controlStructureBody().forEach {
if (it.isIf()) {
count(FeatureName.ELSE_IF, it.toLocation())
seenIfStarts += it.start.startIndex
}
}
if (ctx.ELSE() != null && ctx.controlStructureBody()!!.last().block() != null) {
count(FeatureName.ELSE_STATEMENTS, ctx.ELSE().toLocation())
}
}
override fun exitIfExpression(ctx: KotlinParser.IfExpressionContext) {
if (ctx.start.startIndex in topIfs) {
ifDepths[ifDepths.size - 1]--
check(ifDepth >= 0)
}
}
private val printStatements = setOf("println", "print")
private val javaPrintStatements =
setOf("System.out.println", "System.err.println", "System.out.print", "System.err.print")
private val unnecessaryJavaPrintStatements = setOf("System.out.println", "System.out.print")
private val assertStatements = setOf("assert")
private val requireOrCheckStatements = setOf("require", "check")
private val javaEqualsStatements = setOf("equals")
override fun enterPostfixUnaryExpression(ctx: KotlinParser.PostfixUnaryExpressionContext) {
for (i in 0 until ctx.postfixUnarySuffix().size) {
val current = ctx.postfixUnarySuffix(i)
if (current.navigationSuffix() == null) {
continue
}
val next = if (i == ctx.postfixUnarySuffix().size - 1) {
null
} else {
ctx.postfixUnarySuffix(i + 1)
}
if (current.navigationSuffix().memberAccessOperator()?.DOT() != null &&
current.navigationSuffix().simpleIdentifier() != null
) {
count(FeatureName.DOT_NOTATION, current.navigationSuffix().memberAccessOperator().DOT().toLocation())
if (next?.callSuffix() != null) {
val identifier = current.navigationSuffix().simpleIdentifier().text
count(FeatureName.DOTTED_METHOD_CALL, next.callSuffix().toLocation())
currentFeatures.features.dottedMethodList += identifier
if (identifier in typeCasts) {
count(FeatureName.PRIMITIVE_CASTING, current.navigationSuffix().simpleIdentifier().toLocation())
}
} else {
count(
FeatureName.DOTTED_VARIABLE_ACCESS,
current.navigationSuffix().memberAccessOperator().DOT().toLocation(),
)
}
}
}
}
override fun enterComparisonOperator(ctx: KotlinParser.ComparisonOperatorContext) {
count(FeatureName.COMPARISON_OPERATORS, ctx.toLocation())
}
private enum class SeparatorType {
DOT,
COLONCOLON,
SAFENAV,
UNSAFENAV,
}
private fun String.isArrayType() = this == "Array" ||
this == "arrayOf" ||
basicTypes.any {
this == "${it}Array" || this == "${it.lowercase()}ArrayOf"
}
private var arrayDepth = 0
override fun enterExpression(ctx: KotlinParser.ExpressionContext) {
ctx.DISJ()?.also {
count(FeatureName.LOGICAL_OPERATORS, ctx.DISJ().toLocation())
}
ctx.CONJ()?.also {
count(FeatureName.LOGICAL_OPERATORS, ctx.CONJ().toLocation())
}
ctx.isOperator?.also {
count(FeatureName.INSTANCEOF, ctx.isOperator.toLocation())
}
ctx.asOperator()?.also {
ctx.type().also {
if (it.text in basicTypes) {
count(FeatureName.PRIMITIVE_CASTING, ctx.asOperator().toLocation())
} else {
count(FeatureName.CASTING, ctx.asOperator().toLocation())
}
}
}
ctx.elvis()?.also {
count(FeatureName.ELVIS_OPERATOR, ctx.elvis().toLocation())
}
ctx.RANGE()?.also {
if (inForStatement) {
count(FeatureName.FOR_LOOP_RANGE, ctx.RANGE().toLocation())
}
}
ctx.RANGE_UNTIL()?.also {
if (inForStatement) {
count(FeatureName.FOR_LOOP_RANGE, ctx.RANGE_UNTIL().toLocation())
}
}
val firstSimpleIdentifier = ctx.expression().getOrNull(0)?.primaryExpression()?.simpleIdentifier()?.text
if (firstSimpleIdentifier?.isArrayType() == true) {
if (arrayDepth == 0) {
count(FeatureName.ARRAYS, ctx.toLocation())
count(FeatureName.ARRAY_LITERAL, ctx.toLocation())
} else if (arrayDepth > 0) {
count(FeatureName.MULTIDIMENSIONAL_ARRAYS, ctx.toLocation())
}
arrayDepth++
}
if (ctx.postfixUnarySuffix().isNotEmpty()) {
var skipDots = false
if (firstSimpleIdentifier != null &&
ctx.postfixUnarySuffix().isNotEmpty() &&
ctx.postfixUnarySuffix().last().callSuffix() != null &&
ctx.postfixUnarySuffix().dropLast(1).all { it.navigationSuffix() != null }
) {
val fullMethodCall = firstSimpleIdentifier +
ctx.postfixUnarySuffix()
.dropLast(1)
.joinToString(".") {
it.navigationSuffix().simpleIdentifier().text
}.let {
if (it.isNotBlank()) {
".$it"
} else {
""
}
}
val location = ctx.expression()[0].primaryExpression().simpleIdentifier().toLocation()
val lastChunk = fullMethodCall.split(".").last()
if (printStatements.contains(fullMethodCall)) {
count(FeatureName.PRINT_STATEMENTS, location)
} else if (javaPrintStatements.contains(fullMethodCall)) {
count(FeatureName.PRINT_STATEMENTS, location)
if (unnecessaryJavaPrintStatements.contains(fullMethodCall)) {
count(FeatureName.JAVA_PRINT_STATEMENTS, location)
}
skipDots = true
} else if (assertStatements.contains(fullMethodCall)) {
count(FeatureName.ASSERT, location)
} else if (requireOrCheckStatements.contains(fullMethodCall)) {
count(FeatureName.REQUIRE_OR_CHECK, location)
} else if (javaEqualsStatements.contains(lastChunk)) {
count(FeatureName.EQUALITY, location)
count(FeatureName.JAVA_EQUALITY, location)
} else if (fullMethodCall == "String") {
count(FeatureName.STRING, location)
}
}
if (!skipDots) {
for (i in 0 until ctx.postfixUnarySuffix().size) {
val current = ctx.postfixUnarySuffix(i)
if (current.navigationSuffix() == null) {
continue
}
val next = if (i == ctx.postfixUnarySuffix().size - 1) {
null
} else {
ctx.postfixUnarySuffix(i + 1)
}
val currentSuffix = current.navigationSuffix()
val separatorType = currentSuffix.memberAccessOperator()?.let {
when {
it.DOT() != null -> SeparatorType.DOT
it.COLONCOLON() != null -> SeparatorType.COLONCOLON
it.SAFENAV() != null -> SeparatorType.SAFENAV
it.BANGS_WITH_DOT() != null -> SeparatorType.UNSAFENAV
else -> error("Invalid separator")
}
}
if (separatorType != null && currentSuffix.simpleIdentifier() != null) {
val location = currentSuffix.memberAccessOperator().toLocation()
when (separatorType) {
SeparatorType.DOT -> count(FeatureName.DOT_NOTATION, location)
SeparatorType.SAFENAV -> count(FeatureName.SAFE_CALL_OPERATOR, location)
SeparatorType.UNSAFENAV -> count(FeatureName.UNSAFE_CALL_OPERATOR, location)
// Not counting COLONCOLON yet
else -> {}
}
if (next?.callSuffix() != null) {
val identifier = current.navigationSuffix().simpleIdentifier().text
count(FeatureName.DOTTED_METHOD_CALL, next.callSuffix().toLocation())
currentFeatures.features.dottedMethodList += identifier
if (identifier in typeCasts) {
count(
FeatureName.PRIMITIVE_CASTING,
current.navigationSuffix().simpleIdentifier().toLocation(),
)
}
} else {
count(FeatureName.DOTTED_VARIABLE_ACCESS, location)
}
}
}
}
}
val indices = ctx.postfixUnarySuffix()?.filter { it.indexingSuffix() != null } ?: listOf()
if (indices.isNotEmpty()) {
count(FeatureName.COLLECTION_INDEXING, indices.first().toLocation())
if (indices.size > 1) {
count(FeatureName.MULTILEVEL_COLLECTION_INDEXING, indices.first().toLocation())
}
}
}
override fun exitExpression(ctx: KotlinParser.ExpressionContext) {
val firstSimpleIdentifier = ctx.expression().getOrNull(0)?.primaryExpression()?.simpleIdentifier()?.text
if (firstSimpleIdentifier?.isArrayType() == true) {
arrayDepth--
}
}
private var arrayTypeDepth = 0
override fun enterSimpleUserType(ctx: KotlinParser.SimpleUserTypeContext) {
if (ctx.simpleIdentifier()?.text?.isArrayType() == true) {
if (arrayTypeDepth == 0) {
count(FeatureName.ARRAYS, ctx.toLocation())
} else if (arrayTypeDepth > 0) {
count(FeatureName.MULTIDIMENSIONAL_ARRAYS, ctx.toLocation())
}
arrayTypeDepth++
}
}
override fun exitSimpleUserType(ctx: KotlinParser.SimpleUserTypeContext) {
if (ctx.simpleIdentifier()?.text?.isArrayType() == true) {
arrayTypeDepth--
}
}
override fun enterObjectLiteral(ctx: KotlinParser.ObjectLiteralContext) {
if (ctx.classBody() != null) {
anonymousClassDepth++
objectLiteralCounter++
}
count(FeatureName.ANONYMOUS_CLASSES, ctx.toLocation())
}
override fun exitObjectLiteral(ctx: KotlinParser.ObjectLiteralContext) {
if (ctx.classBody() != null) {
anonymousClassDepth--
}
}
override fun enterImportHeader(ctx: KotlinParser.ImportHeaderContext) {
count(FeatureName.IMPORT, ctx.toLocation())
val importName = ctx.identifier().text + if (ctx.DOT() != null) {
".*"
} else {
""
}
currentFeatures.features.importList += importName
}
override fun enterTypeTest(ctx: KotlinParser.TypeTestContext) {
count(FeatureName.INSTANCEOF, ctx.toLocation())
}
override fun enterPropertyDeclaration(ctx: PropertyDeclarationContext) {
if (ctx.parentType() == ParentType.CLASS) {
count(FeatureName.CLASS_FIELD, ctx.toLocation())
ctx.modifiers()?.modifier()?.find { it.inheritanceModifier()?.ABSTRACT() != null }?.also {
count(FeatureName.ABSTRACT_FIELD, it.toLocation())
}
ctx.VAL()?.also {
count(FeatureName.FINAL_FIELD, ctx.VAL().toLocation())
}
}
}
override fun enterPrimaryConstructor(ctx: KotlinParser.PrimaryConstructorContext) {
count(FeatureName.CONSTRUCTOR, ctx.toLocation())
}
override fun enterClassParameter(ctx: KotlinParser.ClassParameterContext) {
if (ctx.VAL() != null || ctx.VAR() != null) {
count(FeatureName.CLASS_FIELD, ctx.toLocation())
}
}
override fun enterEqualityOperator(ctx: KotlinParser.EqualityOperatorContext) {
if (ctx.EQEQEQ() != null || ctx.EXCL_EQEQ() != null) {
count(FeatureName.REFERENCE_EQUALITY, (ctx.EQEQEQ() ?: ctx.EXCL_EQEQ()).toLocation())
} else {
count(FeatureName.EQUALITY, ctx.toLocation())
}
}
override fun enterCompanionObject(ctx: KotlinParser.CompanionObjectContext) {
count(FeatureName.COMPANION_OBJECT, ctx.toLocation())
ctx.classBody()?.classMemberDeclarations()?.classMemberDeclaration()?.forEach { memberDeclaration ->
memberDeclaration?.declaration()?.functionDeclaration()?.also {
count(FeatureName.STATIC_METHOD, it.toLocation())
}
memberDeclaration?.declaration()?.propertyDeclaration()?.also {
count(FeatureName.STATIC_FIELD, it.toLocation())
}
}
}
override fun enterAssignmentAndOperator(ctx: KotlinParser.AssignmentAndOperatorContext) {
count(FeatureName.ASSIGNMENT_OPERATORS, ctx.toLocation())
}
override fun enterAdditiveOperator(ctx: KotlinParser.AdditiveOperatorContext) {
count(FeatureName.ARITHMETIC_OPERATORS, ctx.toLocation())
}
override fun enterMultiplicativeOperator(ctx: KotlinParser.MultiplicativeOperatorContext) {
count(FeatureName.ARITHMETIC_OPERATORS, ctx.toLocation())
}
override fun enterJumpExpression(ctx: KotlinParser.JumpExpressionContext) {
if (ctx.BREAK() != null || ctx.BREAK_AT() != null) {
count(FeatureName.BREAK, (ctx.BREAK() ?: ctx.BREAK_AT()).toLocation())
} else if (ctx.CONTINUE() != null || ctx.CONTINUE_AT() != null) {
count(FeatureName.CONTINUE, (ctx.CONTINUE() ?: ctx.CONTINUE_AT()).toLocation())
} else if (ctx.RETURN() != null || ctx.RETURN_AT() != null) {
count(FeatureName.RETURN, (ctx.RETURN() ?: ctx.RETURN_AT()).toLocation())
} else if (ctx.THROW() != null) {
count(FeatureName.THROW, ctx.THROW().toLocation())
}
}
override fun enterLiteralConstant(ctx: KotlinParser.LiteralConstantContext) {
if (ctx.NullLiteral() != null) {
count(FeatureName.NULL, ctx.NullLiteral().toLocation())
}
}
override fun enterStringLiteral(ctx: KotlinParser.StringLiteralContext) {
count(FeatureName.STRING, ctx.toLocation())
}
override fun enterWhenExpression(ctx: KotlinParser.WhenExpressionContext) {
count(FeatureName.WHEN_STATEMENT, ctx.toLocation())
if (ctx.start.previousToken() == "=") {
count(FeatureName.WHEN_EXPRESSIONS, ctx.toLocation())
}
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
}
override fun exitWhenExpression(ctx: KotlinParser.WhenExpressionContext?) {
if (lastWhenEntry != null) {
count(FeatureName.LAST_WHEN_ENTRY, lastWhenEntry!!.toLocation())
lastWhenEntry = null
}
}
private var lastWhenEntry: KotlinParser.WhenEntryContext? = null
override fun enterWhenEntry(ctx: KotlinParser.WhenEntryContext) {
count(FeatureName.WHEN_ENTRY, ctx.toLocation())
if (ctx.ELSE() != null) {
count(FeatureName.ELSE_STATEMENTS, ctx.ELSE().toLocation())
}
lastWhenEntry = ctx
}
override fun enterEnumClassBody(ctx: KotlinParser.EnumClassBodyContext) {
count(FeatureName.ENUM, ctx.toLocation())
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
}
override fun enterSetter(ctx: KotlinParser.SetterContext) {
count(FeatureName.SETTER, ctx.toLocation())
ifDepths += 0
}
override fun exitSetter(ctx: KotlinParser.SetterContext) {
ifDepths.pop()
}
override fun enterGetter(ctx: KotlinParser.GetterContext) {
count(FeatureName.GETTER, ctx.toLocation())
ifDepths += 0
}
override fun exitGetter(ctx: KotlinParser.GetterContext?) {
ifDepths.pop()
}
override fun enterThisExpression(ctx: KotlinParser.ThisExpressionContext) {
count(FeatureName.THIS, ctx.toLocation())
}
override fun enterSuperExpression(ctx: KotlinParser.SuperExpressionContext) {
count(FeatureName.SUPER, ctx.toLocation())
}
override fun enterTypeParameters(ctx: KotlinParser.TypeParametersContext) {
ctx.typeParameter().forEach { typeParameter ->
count(FeatureName.TYPE_PARAMETERS, typeParameter.toLocation())
}
}
override fun enterTypeArguments(ctx: KotlinParser.TypeArgumentsContext) {
ctx.typeProjection().forEach { typeProjection ->
count(FeatureName.TYPE_PARAMETERS, typeProjection.toLocation())
}
}
override fun enterVisibilityModifier(ctx: KotlinParser.VisibilityModifierContext) {
count(FeatureName.VISIBILITY_MODIFIERS, ctx.toLocation())
}
override fun enterClassMemberDeclaration(ctx: KotlinParser.ClassMemberDeclarationContext) {
ctx.declaration()?.classDeclaration()?.also {
count(FeatureName.NESTED_CLASS, it.toLocation())
}
ctx.declaration()?.emptyClassDeclaration()?.also {
count(FeatureName.NESTED_CLASS, it.toLocation())
}
}
override fun enterClassBody(ctx: ClassBodyContext) {
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
}
override fun enterInterfaceBody(ctx: KotlinParser.InterfaceBodyContext) {
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
}
override fun enterTryExpression(ctx: KotlinParser.TryExpressionContext) {
count(FeatureName.TRY_BLOCK, ctx.toLocation())
if (ctx.start.previousToken() == "=") {
count(FeatureName.TRY_EXPRESSIONS, ctx.toLocation())
}
ctx.finallyBlock()?.also {
count(FeatureName.FINALLY, it.toLocation())
}
}
override fun enterObjectDeclaration(ctx: KotlinParser.ObjectDeclarationContext) {
count(FeatureName.SINGLETON, ctx.toLocation())
enterClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun exitObjectDeclaration(ctx: KotlinParser.ObjectDeclarationContext) {
exitClassOrInterface(
ctx.simpleIdentifier().text,
Location(ctx.start.line, ctx.start.charPositionInLine),
Location(ctx.stop.line, ctx.stop.charPositionInLine),
)
}
override fun enterLambdaLiteral(ctx: KotlinParser.LambdaLiteralContext) {
count(FeatureName.LAMBDA_EXPRESSIONS, ctx.toLocation())
add(FeatureName.BLOCK_START, ctx.LCURL().toLocation())
add(FeatureName.BLOCK_END, ctx.RCURL().toLocation())
}
override fun enterAnonymousFunction(ctx: KotlinParser.AnonymousFunctionContext) {
count(FeatureName.ANONYMOUS_FUNCTION, ctx.toLocation())
}
override fun enterStatement(ctx: StatementContext) {
add(FeatureName.STATEMENT_START, ctx.start.toLocation())
add(FeatureName.STATEMENT_END, ctx.stop.toLocation())
}
init {
parsedSource = source.getParsed(filename)
// println(parsedSource.tree.format(parsedSource.parser))
ParseTreeWalker.DEFAULT.walk(this, parsedSource.tree)
}
}