66
77package com.datadog.tools.detekt.rules.sdk
88
9- import com.datadog.tools.detekt.ext.fqTypeName
109import com.datadog.tools.detekt.rules.AbstractCallExpressionRule
10+ import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser
11+ import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.CodeParser.KtMethodParameter
12+ import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigParser
13+ import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.DetektConfigValidator
14+ import com.datadog.tools.detekt.rules.sdk.rule.thirdparty.SignatureRule
1115import io.gitlab.arturbosch.detekt.api.CodeSmell
1216import io.gitlab.arturbosch.detekt.api.Config
1317import io.gitlab.arturbosch.detekt.api.Debt
@@ -18,41 +22,31 @@ import io.gitlab.arturbosch.detekt.api.config
1822import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
1923import org.jetbrains.kotlin.psi.KtCallExpression
2024import org.jetbrains.kotlin.psi.KtTryExpression
21- import org.jetbrains.kotlin.resolve.BindingContext
2225import java.util.Stack
2326
2427/* *
25- * This rule will report any call to a "third party" function that is considered unsafe, that is,
26- * which could throw an exception.
28+ * Reports any call to a "third party" function that is considered unsafe (i.e. could throw an
29+ * exception). Third party functions are detected based on an internal package prefix: any method
30+ * with that prefix is treated as first party.
2731 *
28- * Third party functions are detected based on an internal package prefix: any method which has a
29- * package name with this prefix is considered first party, anything else is third party.
32+ * The decision logic lives in this rule. Config-string parsing is delegated to [DetektConfigParser]
33+ * and PSI-level extraction to [CodeParser];
34+ * both feed into [com.datadog.tools.detekt.rules.sdk.rule.thirdparty.SignatureRule] which is the
35+ * single abstraction matching/validating either direct or wildcarded YAML records.
3036 */
3137@RequiresTypeResolution
3238class UnsafeThirdPartyFunctionCall (
3339 config : Config
3440) : AbstractCallExpressionRule(config, includeTypeArguments = false ) {
3541
42+ private val codeParser = CodeParser ()
43+ private val ruleParser = DetektConfigParser ()
44+ private val configValidator = DetektConfigValidator (ruleParser)
3645 private val internalPackagePrefix: String by config(defaultValue = " " )
3746 private val treatUnknownFunctionAsThrowing: Boolean by config(defaultValue = true )
38- private val knownThrowingCalls: List <String > by config(defaultValue = emptyList())
39- private val knownSafeCalls: List <String > by config(defaultValue = emptyList())
40-
41- private val knownThrowingCallsMap: Map <String , List <String >> by lazy {
42- knownThrowingCalls.map {
43- val splitColon = it.split(' :' )
44- val key = splitColon.first()
45- if (splitColon.size == 1 ) {
46- println (" ✘ ERROR WITH KNOWN THROWING CALL: $it " )
47- }
48- val exceptions = splitColon[1 ].split(' ,' ).toList()
49- key to exceptions
50- }.toMap()
51- }
52-
53- private val caughtExceptions = Stack <List <String >>()
54-
55- // region Rule
47+ private val knownSafeCalls: List <SignatureRule > by config(emptyList(), ruleParser::parseSafeCalls)
48+ private val knownThrowingCalls: List <SignatureRule > by config(emptyList(), ruleParser::parseThrowingCalls)
49+ private val caughtExceptions = Stack <Set <String >>()
5650
5751 override val issue: Issue = Issue (
5852 javaClass.simpleName,
@@ -62,85 +56,81 @@ class UnsafeThirdPartyFunctionCall(
6256 Debt .TWENTY_MINS
6357 )
6458
59+ init {
60+ configValidator.validate(knownSafeCalls, knownThrowingCalls)
61+ }
62+
6563 override fun visitTryExpression (expression : KtTryExpression ) {
66- val caughtTypes = expression.catchClauses
67- .mapNotNull {
68- val typeReference = it.catchParameter?.typeReference
69- bindingContext.get(BindingContext .TYPE , typeReference)?.fqTypeName()
70- }
71- caughtExceptions.push(caughtTypes)
64+ caughtExceptions.push(codeParser.parseCaughtTypes(expression, bindingContext))
7265 super .visitTryExpression(expression)
7366 caughtExceptions.pop()
7467 }
7568
76- // endregion
77-
78- // region AbstractCallExpressionRule
79-
8069 @Suppress(" ReturnCount" )
8170 override fun visitResolvedFunctionCall (
8271 expression : KtCallExpression ,
8372 resolvedCall : ResolvedFunCall
8473 ) {
85- if (internalPackagePrefix.isNotEmpty()) {
86- val belongsToInternalContainer = resolvedCall.containerFqName.startsWith(internalPackagePrefix) ||
87- resolvedCall.containingPackage.startsWith(internalPackagePrefix)
88- if (belongsToInternalContainer) return
89- }
90- if (resolvedCall.functionName in kotlinHelperMethods) {
74+ if (resolvedCall.functionName in kotlinHelperMethods ||
75+ resolvedCall.isBelongsToInternalPrefix(internalPackagePrefix)
76+ ) {
9177 return
9278 }
9379
94- if (knownThrowingCallsMap.containsKey(resolvedCall.call)) {
95- val knownThrowables = knownThrowingCallsMap[resolvedCall.call] ? : emptyList()
96- checkCallThrowingExceptions(expression, resolvedCall.call, knownThrowables)
97- } else if (treatUnknownFunctionAsThrowing && ! knownSafeCalls.contains(resolvedCall.call)) {
98- val message = " Calling ${resolvedCall.call} could throw exceptions, but this method is unknown"
99- reportUnsafeCall(expression, message)
100- }
80+ classifyAndReport(
81+ expression,
82+ signature = resolvedCall.call,
83+ params = codeParser.parseFormalParams(expression, bindingContext)
84+ )
10185 }
10286
103- // endregion
87+ private fun classifyAndReport (expression : KtCallExpression , signature : String , params : List <KtMethodParameter >) {
88+ knownThrowingCalls.firstOrNull { it.matches(signature) }
89+ ?.let { throwingMatch ->
90+ throwingMatch.validate(params)
91+ checkCallThrowingExceptions(expression, signature, throwingMatch.exceptions)
92+ return
93+ }
10494
105- // region Internal
95+ knownSafeCalls.firstOrNull { it.matches(signature) }
96+ ?.let { safeMatch ->
97+ safeMatch.validate(params)
98+ return
99+ }
100+
101+ if (treatUnknownFunctionAsThrowing) {
102+ reportUnsafeCall(
103+ expression,
104+ " Calling $signature could throw exceptions, but this method is unknown"
105+ )
106+ }
107+ }
106108
107109 private fun checkCallThrowingExceptions (
108110 expression : KtCallExpression ,
109111 call : String ,
110112 exceptions : List <String >
111113 ) {
112- val catchesAnyException = caughtExceptions.any { list ->
113- list.any { e -> e in topLevelExceptions }
114- }
115- val catchesAnyError = caughtExceptions.any { list ->
116- list.any { e -> e in topLevelErrors }
117- }
118- val uncaught = exceptions.filter { exception ->
119- caughtExceptions.none { it.contains(exception) }
114+ val catchesAnyException = caughtExceptions.any { list -> list.any { e -> e in topLevelExceptions } }
115+ val catchesAnyError = caughtExceptions.any { list -> list.any { e -> e in topLevelErrors } }
116+ val uncaught = exceptions.filter { exception -> caughtExceptions.none { it.contains(exception) } }.filter {
117+ val isUncaughtException = ! catchesAnyException && it.endsWith(" Exception" )
118+ val isUncaughtError = ! catchesAnyError && it.endsWith(" Error" )
119+ isUncaughtException || isUncaughtError
120120 }
121- .filter {
122- val isUncaughtException = it.endsWith(" Exception" ) && ! catchesAnyException
123- val isUncaughtError = it.endsWith(" Error" ) && ! catchesAnyError
124- isUncaughtException || isUncaughtError
125- }
126121
127- if (uncaught.isEmpty()) {
128- return
129- }
122+ if (uncaught.isEmpty()) return
130123
131- val msg = " Calling $call can throw the following exceptions: ${exceptions.joinToString()} ."
132- reportUnsafeCall(expression, msg)
124+ reportUnsafeCall(
125+ expression = expression,
126+ message = " Calling $call can throw the following exceptions: ${exceptions.joinToString()} ."
127+ )
133128 }
134129
135- private fun reportUnsafeCall (
136- expression : KtCallExpression ,
137- message : String
138- ) {
130+ private fun reportUnsafeCall (expression : KtCallExpression , message : String ) {
139131 report(CodeSmell (issue, Entity .from(expression), message = message))
140132 }
141133
142- // endregion
143-
144134 companion object {
145135 private const val JAVA_EXCEPTION_CLASS = " java.lang.Exception"
146136 private const val JAVA_ERROR_CLASS = " java.lang.Error"
@@ -167,5 +157,14 @@ class UnsafeThirdPartyFunctionCall(
167157 " let" , " run" , " with" , " apply" , " also" ,
168158 " print" , " println" , " toString" , " invoke"
169159 )
160+
161+ private fun ResolvedFunCall.isBelongsToInternalPrefix (
162+ internalPackagePrefix : String
163+ ): Boolean {
164+ val isInternal =
165+ containerFqName.startsWith(internalPackagePrefix) || containingPackage.startsWith(internalPackagePrefix)
166+
167+ return internalPackagePrefix.isNotEmpty() && isInternal
168+ }
170169 }
171170}
0 commit comments