Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

SONARKT-500 Migrate WebViewsFileAccessCheck to kotlin-analysis-api #543

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ fun KtExpression?.isLocalVariable(): Boolean = withKaSession {
return mainReference.resolveToSymbol() is KaLocalVariableSymbol
}

@Deprecated("use kotlin-analysis-api instead")
fun KtExpression?.setterMatches(bindingContext: BindingContext, propertyName: String, matcher: FunMatcherImpl): Boolean = when (this) {
is KtNameReferenceExpression -> (getReferencedName() == propertyName) &&
(matcher.matches((bindingContext[BindingContext.REFERENCE_TARGET, this] as? PropertyDescriptor)?.unwrappedSetMethod))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.resolution.KaCallableMemberCall
import org.jetbrains.kotlin.analysis.api.resolution.KaFunctionCall
import org.jetbrains.kotlin.analysis.api.resolution.KaReceiverValue
import org.jetbrains.kotlin.analysis.api.resolution.KaVariableAccessCall
import org.jetbrains.kotlin.analysis.api.resolution.KaSimpleVariableAccess
import org.jetbrains.kotlin.analysis.api.resolution.KaSimpleVariableAccessCall
import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull
import org.jetbrains.kotlin.analysis.api.resolution.symbol
import org.jetbrains.kotlin.analysis.api.signatures.KaCallableSignature
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaSyntheticJavaPropertySymbol
import org.jetbrains.kotlin.analysis.api.symbols.name
import org.jetbrains.kotlin.analysis.api.types.KaClassType
import org.jetbrains.kotlin.backend.common.descriptors.isSuspend
Expand All @@ -39,9 +44,9 @@ import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingContext.RESOLVED_CALL
import org.jetbrains.kotlin.resolve.calls.util.getCall
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.tasks.isDynamic
import org.jetbrains.kotlin.resolve.calls.util.getCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension
import org.jetbrains.kotlin.resolve.descriptorUtil.overriddenTreeUniqueAsSequence
Expand Down Expand Up @@ -91,7 +96,8 @@ class FunMatcherImpl(

@OptIn(KaExperimentalApi::class)
fun matches(node: KtNamedFunction): Boolean = withKaSession {
return matches(null, node.symbol.asSignature())
val symbol = node.symbol
return matches(null, symbol, symbol.asSignature())
}

@Deprecated("use kotlin-analysis-api instead")
Expand All @@ -102,17 +108,57 @@ class FunMatcherImpl(
fun matches(resolvedCall: ResolvedCall<*>?) = preCheckArgumentCount(resolvedCall?.call) &&
matches(resolvedCall?.resultingDescriptor)

fun matches(resolvedCall: KaCallableMemberCall<*, *>?) =
@OptIn(KaExperimentalApi::class)
fun matches(resolvedCall: KaCallableMemberCall<*, *>?): Boolean = withKaSession {
when (resolvedCall) {
is KaFunctionCall -> matches(
resolvedCall.partiallyAppliedSymbol.dispatchReceiver,
resolvedCall.partiallyAppliedSymbol.signature
)
is KaVariableAccessCall ->
// TODO handle calls of getters and setters
false
else -> false
null -> false
is KaFunctionCall -> {
val symbol = resolvedCall.partiallyAppliedSymbol
matches(symbol.dispatchReceiver, symbol.signature.symbol, symbol.signature)
}
is KaSimpleVariableAccessCall -> {
val propertySymbol = (resolvedCall.symbol as? KaPropertySymbol) ?: return false
when (resolvedCall.simpleAccess) {
is KaSimpleVariableAccess.Read -> {
val symbolForNameCheck =
/**
* Note that this allows to use matcher with name `"getExample"`
* for both function call `getExample(...)` and read by property name `example`.
*/
if (propertySymbol is KaSyntheticJavaPropertySymbol)
propertySymbol.javaGetterSymbol
else
propertySymbol
val getterSignature = propertySymbol.getter?.asSignature() ?: return false
checkName(symbolForNameCheck) &&
checkCallParameters(getterSignature) &&
checkReturnType(getterSignature) &&
checkTypeOrSupertype(null, propertySymbol)
}
is KaSimpleVariableAccess.Write -> {
val symbolForNameCheck =
/**
* Note that this allows to use matcher with name `"setExample"`
* for both function call `setExample(...)` and write by property name `example`.
*/
if (propertySymbol is KaSyntheticJavaPropertySymbol)
propertySymbol.javaSetterSymbol ?: return false
else
propertySymbol
val setterSignature = propertySymbol.setter?.asSignature() ?: return false
checkName(symbolForNameCheck) &&
checkCallParameters(setterSignature) &&
checkReturnType(setterSignature) &&
checkTypeOrSupertype(
null,
// TODO propertySymbol works only in K2 (see ScheduledThreadPoolExecutorZeroCheck):
symbolForNameCheck
)
}
}
}
}
}

private fun preCheckArgumentCount(call: Call?) = call == null || call.valueArguments.size <= maxArgumentCount

Expand All @@ -130,6 +176,7 @@ class FunMatcherImpl(

private fun matches(
dispatchReceiver: KaReceiverValue?,
symbol: KaCallableSymbol,
callableSignature: KaCallableSignature<*>
): Boolean {
if (isDynamic != null) TODO()
Expand All @@ -138,8 +185,8 @@ class FunMatcherImpl(
checkIsSuspending(callableSignature) &&
checkIsOperator(callableSignature) &&
checkReturnType(callableSignature) &&
checkName(callableSignature) &&
checkTypeOrSupertype(dispatchReceiver, callableSignature) &&
checkName(symbol) &&
checkTypeOrSupertype(dispatchReceiver, symbol) &&
checkCallParameters(callableSignature)
}

Expand All @@ -150,17 +197,17 @@ class FunMatcherImpl(

private fun checkTypeOrSupertype(
dispatchReceiver: KaReceiverValue?,
callableSignature: KaCallableSignature<*>,
symbol: KaCallableSymbol,
): Boolean {
if (qualifiersOrDefiningSupertypes.isEmpty()) return true

// TODO try to use only dispatchReceiver?
// java.lang.Math.random has kotlin/Unit receiver type in K2 and null in K1?
val receiverFQN = (dispatchReceiver?.type as? KaClassType)?.classId?.asFqNameString();
if (qualifiersOrDefiningSupertypes.contains(receiverFQN)) return true
if (qualifiersOrDefiningSupertypes.contains(getActualQualifier(callableSignature))) return true
if (qualifiersOrDefiningSupertypes.contains(getActualQualifier(symbol))) return true

if (!definingSupertypes.isNullOrEmpty() && checkSubType(callableSignature)) return true
if (!definingSupertypes.isNullOrEmpty() && checkSubType(symbol)) return true
return false
}

Expand All @@ -174,8 +221,7 @@ class FunMatcherImpl(

// TODO when null?
/** @return dot-separated package name for top-level functions, class name otherwise */
private fun getActualQualifier(callableSignature: KaCallableSignature<*>): String? {
val symbol = callableSignature.symbol
private fun getActualQualifier(symbol: KaCallableSymbol): String? {
return if (symbol is KaConstructorSymbol) {
symbol.containingClassId?.asFqNameString()
} else {
Expand All @@ -201,9 +247,9 @@ class FunMatcherImpl(
}
}

private fun checkSubType(callableSignature: KaCallableSignature<*>): Boolean = withKaSession {
if (callableSignature.symbol is KaConstructorSymbol) return false
return callableSignature.symbol.allOverriddenSymbols.any {
private fun checkSubType(symbol: KaCallableSymbol): Boolean = withKaSession {
if (symbol is KaConstructorSymbol) return false
return symbol.allOverriddenSymbols.any {
val className: String? = it.callableId?.asSingleFqName()?.parent()?.asString()
definingSupertypes.contains(className)
}
Expand All @@ -218,9 +264,9 @@ class FunMatcherImpl(
(nameRegex == null && names.isEmpty()) || name in names || (nameRegex != null && nameRegex.matches(name))
} else false

private fun checkName(callableSignature: KaCallableSignature<*>): Boolean {
if (matchConstructor) return callableSignature.symbol is KaConstructorSymbol
val name = callableSignature.symbol.name?.asString() ?: return false
private fun checkName(symbol: KaCallableSymbol): Boolean {
if (matchConstructor) return symbol is KaConstructorSymbol
val name = symbol.name?.asString() ?: return false
return (nameRegex == null && names.isEmpty()) || name in names || (nameRegex != null && nameRegex.matches(name))
}

Expand Down Expand Up @@ -265,6 +311,11 @@ class FunMatcherImpl(
else null
} ?: true

/**
* Note that [KaCallableSignature] carries use-site type information,
* so for `fun <T> example(): T` matcher with [returnType] `"kotlin.String"`
* matches calls of `example<String>()`.
*/
private fun checkReturnType(callableSignature: KaCallableSignature<*>) =
returnType?.let {
it == callableSignature.returnType.asFqNameString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,23 @@
*/
package org.sonarsource.kotlin.checks

import org.jetbrains.kotlin.analysis.api.resolution.KaCallableMemberCall
import org.jetbrains.kotlin.analysis.api.resolution.KaFunctionCall
import org.jetbrains.kotlin.analysis.api.resolution.successfulCallOrNull
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtPsiUtil
import org.jetbrains.kotlin.resolve.calls.util.getFirstArgumentExpression
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.sonar.check.Rule
import org.sonarsource.kotlin.api.checks.CallAbstractCheck
import org.sonarsource.kotlin.api.checks.FunMatcher
import org.sonarsource.kotlin.api.checks.getFirstArgumentExpression
import org.sonarsource.kotlin.api.checks.predictRuntimeBooleanValue
import org.sonarsource.kotlin.api.checks.setterMatches
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
import org.sonarsource.kotlin.api.visiting.withKaSession

private const val MESSAGE = "Make sure that enabling file access is safe here."

private val PROPERTY_NAMES = arrayOf(
"allowFileAccess",
"allowFileAccessFromFileURLs",
"allowContentAccess",
"allowUniversalAccessFromFileURLs",
).asSequence()

private val ANDROID_FILE_ACCESS_MATCHER = FunMatcher(definingSupertype = "android.webkit.WebSettings") {
withNames(
"setAllowFileAccess",
Expand All @@ -49,27 +43,27 @@ private val ANDROID_FILE_ACCESS_MATCHER = FunMatcher(definingSupertype = "androi
withArguments("kotlin.Boolean")
}

@org.sonarsource.kotlin.api.frontend.K1only
@Rule(key = "S6363")
class WebViewsFileAccessCheck : CallAbstractCheck() {

override val functionsToVisit = listOf(ANDROID_FILE_ACCESS_MATCHER)

override fun visitFunctionCall(callExpression: KtCallExpression, resolvedCall: ResolvedCall<*>, kotlinFileContext: KotlinFileContext) {
override fun visitFunctionCall(callExpression: KtCallExpression, resolvedCall: KaFunctionCall<*>, kotlinFileContext: KotlinFileContext) {
checkFileAccessArgument(kotlinFileContext, resolvedCall.getFirstArgumentExpression())
}

override fun visitBinaryExpression(expression: KtBinaryExpression, ctx: KotlinFileContext) {
val left = KtPsiUtil.deparenthesize(expression.left) ?: return
if (expression.operationToken == KtTokens.EQ) {
PROPERTY_NAMES
.firstOrNull { propertyName -> left.setterMatches(ctx.bindingContext, propertyName, ANDROID_FILE_ACCESS_MATCHER) }
?.let { checkFileAccessArgument(ctx, expression.right) }
override fun visitBinaryExpression(expression: KtBinaryExpression, ctx: KotlinFileContext) = withKaSession {
if (expression.operationToken == KtTokens.EQ
&& ANDROID_FILE_ACCESS_MATCHER.matches(
expression.resolveToCall()?.successfulCallOrNull<KaCallableMemberCall<*, *>>()
)
) {
checkFileAccessArgument(ctx, expression.right)
}
}

private fun checkFileAccessArgument(ctx: KotlinFileContext, argument: KtExpression?) {
if (argument?.predictRuntimeBooleanValue(ctx.bindingContext) == true) {
if (argument?.predictRuntimeBooleanValue() == true) {
ctx.reportIssue(argument, MESSAGE)
}
}
Expand Down