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

Rule 4.1.1 float arithmetic #303

Merged
merged 11 commits into from
Sep 24, 2020
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
![diKTat code style](https://github.com/cqfn/diKTat/workflows/Run%20diKTat/badge.svg)

[![Releases](https://img.shields.io/github/v/release/cqfn/diKTat)](https://github.com/cqfn/diKTat/releases)
![Maven Central](https://img.shields.io/maven-central/v/org.cqfn.diktat/diktat-rules)
[![License](https://img.shields.io/github/license/cqfn/diKtat)](https://github.com/cqfn/diKTat/blob/master/LICENSE)
[![Hits-of-Code](https://hitsofcode.com/github/cqfn/diktat)](https://hitsofcode.com/view/github/cqfn/diktat)
[![codecov](https://codecov.io/gh/cqfn/diKTat/branch/master/graph/badge.svg)](https://codecov.io/gh/cqfn/diKTat)
Expand Down
4 changes: 4 additions & 0 deletions diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,5 +250,9 @@
configuration: {}
# Inspection that checks if string template has redundant quotes
- name: STRING_TEMPLATE_QUOTES
enabled: true
configuration: {}
# Checks that floating-point values are not used in arithmetic expressions
- name: FLOAT_IN_ACCURATE_CALCULATIONS
enabled: true
configuration: {}
2 changes: 2 additions & 0 deletions diktat-rules/src/main/kotlin/generated/WarningNames.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,6 @@ object WarningNames {
const val STRING_TEMPLATE_CURLY_BRACES: String = "STRING_TEMPLATE_CURLY_BRACES"

const val STRING_TEMPLATE_QUOTES: String = "STRING_TEMPLATE_QUOTES"

const val FLOAT_IN_ACCURATE_CALCULATIONS: String = "FLOAT_IN_ACCURATE_CALCULATIONS"
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ enum class Warnings(private val canBeAutoCorrected: Boolean, private val warn: S
TYPE_ALIAS(false, "variable's type is too complex and should be replaced with typealias"),
STRING_TEMPLATE_CURLY_BRACES(true, "string template has redundant curly braces"),
STRING_TEMPLATE_QUOTES(true, "string template has redundant quotes"),
// FixMe: change float literal to BigDecimal? Or kotlin equivalent?
FLOAT_IN_ACCURATE_CALCULATIONS(false, "floating-point values shouldn't be used in accurate calculations"),
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.cqfn.diktat.ruleset.rules
import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider
import org.cqfn.diktat.common.config.rules.RulesConfigReader
import org.cqfn.diktat.ruleset.rules.calculations.AccurateCalculationsRule
import org.cqfn.diktat.ruleset.rules.comments.CommentsRule
import org.cqfn.diktat.ruleset.rules.comments.HeaderCommentRule
import org.cqfn.diktat.ruleset.rules.files.BlankLinesRule
Expand Down Expand Up @@ -49,6 +50,7 @@ class DiktatRuleSetProvider(private val diktatConfigFile: String = "diktat-analy
::HeaderCommentRule,
::SortRule,
::StringConcatenationRule,
::AccurateCalculationsRule,
::LineLength,
::TypeAliasRule,
::BlankLinesRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.cqfn.diktat.ruleset.rules.calculations

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import org.cqfn.diktat.common.config.rules.RulesConfig
import org.cqfn.diktat.ruleset.constants.Warnings.FLOAT_IN_ACCURATE_CALCULATIONS
import org.cqfn.diktat.ruleset.utils.findLocalDeclaration
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.startOffset

/**
* Rule that checks that floating-point numbers are not used for accurate calculations
* 1. Checks that floating-point numbers are not used in arithmetic binary expressions
* Fixme: detect variables by type, not only floating-point literals
*/
class AccurateCalculationsRule(private val configRules: List<RulesConfig>) : Rule("accurate-calculations") {
private lateinit var emitWarn: ((offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit)
private var isFixMode: Boolean = false

companion object {
private val arithmeticOperationTokens = listOf(KtTokens.PLUS, KtTokens.PLUSEQ, KtTokens.PLUSPLUS,
KtTokens.MINUS, KtTokens.MINUSEQ, KtTokens.MINUSMINUS,
KtTokens.MUL, KtTokens.MULTEQ, KtTokens.DIV, KtTokens.DIVEQ,
KtTokens.PERC, KtTokens.PERCEQ,
KtTokens.GT, KtTokens.LT, KtTokens.LTEQ, KtTokens.GTEQ,
KtTokens.EQEQ
)
private val arithmeticOperationsFunctions = listOf("equals", "compareTo")
}

override fun visit(node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {
emitWarn = emit
isFixMode = autoCorrect

when (val psi = node.psi) {
is KtBinaryExpression -> handleBinaryExpression(psi)
is KtDotQualifiedExpression -> handleFunction(psi)
else -> return
}
}

@Suppress("UnsafeCallOnNullableType")
private fun handleBinaryExpression(expression: KtBinaryExpression) = expression
.takeIf { it.operationToken in arithmeticOperationTokens }
?.let { expr ->
// !! is safe because `KtBinaryExpression#left` is annotated `Nullable IfNotParsed`
val floatValue = expr.left!!.takeIf { it.isFloatingPoint() }
?: expr.right!!.takeIf { it.isFloatingPoint() }
checkFloatValue(floatValue, expr)
}

private fun handleFunction(expression: KtDotQualifiedExpression) = expression
.takeIf { it.selectorExpression is KtCallExpression }
?.run { receiverExpression to selectorExpression as KtCallExpression }
?.takeIf {
(it.second.calleeExpression as? KtNameReferenceExpression)
?.getReferencedName() in arithmeticOperationsFunctions
}
?.let { (receiverExpression, selectorExpression) ->
val floatValue = receiverExpression.takeIf { it.isFloatingPoint() }
?: selectorExpression
.valueArguments
.find { it.getArgumentExpression()?.isFloatingPoint() ?: false }

checkFloatValue(floatValue, expression)
}

private fun checkFloatValue(floatValue: PsiElement?, expression: KtExpression) {
if (floatValue != null) {
// float value is used in comparison
FLOAT_IN_ACCURATE_CALCULATIONS.warn(configRules, emitWarn, isFixMode,
"float value of <${floatValue.text}> used in arithmetic expression in ${expression.text}", expression.startOffset, expression.node)
}
}
}

private fun PsiElement.isFloatingPoint() =
node.elementType == ElementType.FLOAT_LITERAL ||
petertrr marked this conversation as resolved.
Show resolved Hide resolved
node.elementType == ElementType.FLOAT_CONSTANT ||
((this as? KtNameReferenceExpression)
?.findLocalDeclaration()
?.initializer
?.node
?.run { elementType == ElementType.FLOAT_LITERAL || elementType == ElementType.FLOAT_CONSTANT }
?: false)
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,22 @@ fun PsiElement.isContainingScope(block: KtBlockExpression): Boolean {
}
return isAncestor(block, false)
}

/**
* Method that tries to find a local property declaration with the same name as current [KtNameReferenceExpression] element
*/
fun KtNameReferenceExpression.findLocalDeclaration(): KtProperty? = parents
.mapNotNull { it as? KtBlockExpression }
.mapNotNull { blockExpression ->
blockExpression
.statements
.takeWhile { !it.isAncestor(this, true) }
.mapNotNull { it as? KtProperty }
.find {
it.isLocal &&
it.hasInitializer() &&
it.name?.equals(getReferencedName())
?: false
}
}
.firstOrNull()
4 changes: 4 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis-huawei.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,5 +250,9 @@
configuration: {}
# Inspection that checks if string template has redundant quotes
- name: STRING_TEMPLATE_QUOTES
enabled: true
configuration: {}
# Checks that floating-point values are not used in arithmetic expressions
- name: FLOAT_IN_ACCURATE_CALCULATIONS
enabled: true
configuration: {}
4 changes: 4 additions & 0 deletions diktat-rules/src/main/resources/diktat-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,9 @@
configuration: {}
# Inspection that checks if string template has redundant quotes
- name: STRING_TEMPLATE_QUOTES
enabled: true
configuration: {}
# Checks that floating-point values are not used in arithmetic expressions
- name: FLOAT_IN_ACCURATE_CALCULATIONS
enabled: true
configuration: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.cqfn.diktat.ruleset.chapter4

import com.pinterest.ktlint.core.LintError
import generated.WarningNames
import org.cqfn.diktat.ruleset.constants.Warnings.FLOAT_IN_ACCURATE_CALCULATIONS
import org.cqfn.diktat.ruleset.rules.DIKTAT_RULE_SET_ID
import org.cqfn.diktat.ruleset.rules.calculations.AccurateCalculationsRule
import org.cqfn.diktat.util.LintTestBase
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

class AccurateCalculationsWarnTest : LintTestBase(::AccurateCalculationsRule) {
private val ruleId = "$DIKTAT_RULE_SET_ID:accurate-calculations"

private fun warnText(ref: String, expr: String) = "${FLOAT_IN_ACCURATE_CALCULATIONS.warnText()} float value of <$ref> used in arithmetic expression in $expr"

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect comparison (equals) with float literal`() {
lintMethod(
"""
|class Example {
| fun foo() {
| x == 1.0
| 1.0 == x
| x.equals(1.0)
| 1.0.equals(x)
| }
|}
""".trimMargin(),
LintError(3, 9, ruleId, warnText("1.0", "x == 1.0"), false),
LintError(4, 9, ruleId, warnText("1.0", "1.0 == x"), false),
LintError(5, 9, ruleId, warnText("1.0", "x.equals(1.0)"), false),
LintError(6, 9, ruleId, warnText("1.0", "1.0.equals(x)"), false)
)
}

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect comparison with float literal`() {
lintMethod(
"""
|class Example {
| fun foo() {
| x > 1.0
| 1.0 > x
| x.compareTo(1.0)
| 1.0.compareTo(x)
| }
|}
""".trimMargin(),
LintError(3, 9, ruleId, warnText("1.0", "x > 1.0"), false),
LintError(4, 9, ruleId, warnText("1.0", "1.0 > x"), false),
LintError(5, 9, ruleId, warnText("1.0", "x.compareTo(1.0)"), false),
LintError(6, 9, ruleId, warnText("1.0", "1.0.compareTo(x)"), false)
)
}

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect comparisons with local floating-point variables - 1`() {
lintMethod(
"""
|class Example {
| fun foo() {
| val x = 1.0
| x == 1
| }
|}
""".trimMargin(),
LintError(4, 9, ruleId, warnText("x", "x == 1"), false)
)
}

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect comparisons with local floating-point variables - 2`() {
lintMethod(
"""
|class Example {
| fun foo() {
| val x = 1L
| list.forEach {
| val x = 1.0
| x == 1
| }
| }
|}
""".trimMargin(),
LintError(6, 13, ruleId, warnText("x", "x == 1"), false)
)
}

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect comparisons with local floating-point variables - 3`() {
lintMethod(
"""
|class Example {
| fun foo() {
| val x = 1L
| list.forEach {
| obj.let {
| x == 1
| }
| val x = 1.0
| }
| }
|}
""".trimMargin()
)
}

@Test
@Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS)
fun `should detect different operations with operators`() {
lintMethod(
"""
|class Example {
| fun foo() {
| val x = 1.0
| x == 1
| x + 2
| // x++
| x += 2
| x - 2
| // x--
| x -= 2
| x * 2
| x *= 2
| x / 2
| x /= 2
| x % 2
| x %= 2
| }
|}
""".trimMargin(),
LintError(4, 9, ruleId, warnText("x", "x == 1"), false),
LintError(5, 9, ruleId, warnText("x", "x + 2"), false),
// LintError(6, 9, ruleId, warnText("x", "x++"), false),
LintError(7, 9, ruleId, warnText("x", "x += 2"), false),
LintError(8, 9, ruleId, warnText("x", "x - 2"), false),
// LintError(9, 9, ruleId, warnText("x", "x--"), false),
LintError(10, 9, ruleId, warnText("x", "x -= 2"), false),
LintError(11, 9, ruleId, warnText("x", "x * 2"), false),
LintError(12, 9, ruleId, warnText("x", "x *= 2"), false),
LintError(13, 9, ruleId, warnText("x", "x / 2"), false),
LintError(14, 9, ruleId, warnText("x", "x /= 2"), false),
LintError(15, 9, ruleId, warnText("x", "x % 2"), false),
LintError(16, 9, ruleId, warnText("x", "x %= 2"), false)
)
}
}
1 change: 1 addition & 0 deletions info/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@
| 3 | 3.14.2 | STRING_TEMPLATE_CURLY_BRACES | Check: warns if there is redundant curly braces in string template<br> Fix: deletes curly braces | yes | - | + |
| 3 | 3.14.2 | STRING_TEMPLATE_QUOTES | Check: warns if there are redundant quotes in string template<br> Fix: deletes quotes and $ symbol | yes | - | + |
| 4 | 4.2.2| TYPE_ALIAS | Check: if type reference of property is longer than expected | yes | typeReferenceLength | -|
| 4 | 4.1.1 | FLOAT_IN_ACCURATE_CALCULATIONS | Checks that floating-point values are not used in arithmetic expressions<br>Fix: no | no | - | Current implementation detects only floating-point constants |