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 @@ -229,5 +229,9 @@
enabled: true
configuration: {}
- name: LOCAL_VARIABLE_EARLY_DECLARATION
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 @@ -147,4 +147,6 @@ object WarningNames {
const val WRONG_MULTIPLE_MODIFIERS_ORDER: String = "WRONG_MULTIPLE_MODIFIERS_ORDER"

const val LOCAL_VARIABLE_EARLY_DECLARATION: String = "LOCAL_VARIABLE_EARLY_DECLARATION"

const val FLOAT_IN_ACCURATE_CALCULATIONS: String = "FLOAT_IN_ACCURATE_CALCULATIONS"
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ enum class Warnings(private val canBeAutoCorrected: Boolean, private val warn: S
WRONG_DECLARATIONS_ORDER(true, "declarations of constants and enum members should be sorted alphabetically"),
WRONG_MULTIPLE_MODIFIERS_ORDER(true, "sequence of modifiers is incorrect"),
LOCAL_VARIABLE_EARLY_DECLARATION(false, "local variables should be declared close to the line where they are first used"),
// 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 @@ -48,6 +49,7 @@ class DiktatRuleSetProvider(private val diktatConfigFile: String = "diktat-analy
::HeaderCommentRule,
::SortRule,
::StringConcatenationRule,
::AccurateCalculationsRule,
::LineLength,
::BlankLinesRule,
::WhiteSpaceRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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.backend.common.onlyIf
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.KtBlockExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.parents
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") {
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.EQEQ
)
}

@Suppress("UnsafeCallOnNullableType")
override fun visit(node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {
(node.psi as? KtBinaryExpression)?.onlyIf({ operationToken in arithmeticOperationTokens }) { expr ->
petertrr marked this conversation as resolved.
Show resolved Hide resolved
// !! is safe because `KtBinaryExpression#left` is annotated `Nullable IfNotParsed`
val floatValue = expr.left!!.takeIf { it.isFloatingPoint() } ?: expr.right!!.takeIf { it.isFloatingPoint() }
if (floatValue != null) {
// float value is used in comparison
FLOAT_IN_ACCURATE_CALCULATIONS.warn(configRules, emit, autoCorrect,
"float value of <${floatValue.text}> used in arithmetic expression in ${expr.text}", expr.startOffset, 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 @@ -229,5 +229,9 @@
enabled: true
configuration: {}
- name: LOCAL_VARIABLE_EARLY_DECLARATION
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 @@ -233,5 +233,9 @@
configuration: {}
# Inspection that checks if local variables are declared close to the first usage site
- name: LOCAL_VARIABLE_EARLY_DECLARATION
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,124 @@
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 with float literal`() {
lintMethod(
"""
|class Example {
| fun foo() {
| x == 1.0
| 1.0 == x
| }
|}
""".trimMargin(),
LintError(3, 9, ruleId, warnText("1.0", "x == 1.0"), false),
LintError(4, 9, ruleId, warnText("1.0", "1.0 == 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
| }
|}
""".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)
)
}
}
1 change: 1 addition & 0 deletions info/available-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@
| 3 | 3.2 | WRONG_DECLARATIONS_ORDER | Check: if order of enum values or constant property inside companion isn't correct | yes | - | - |
| 3 | 3.11 | ANNOTATION_NEW_LINE | Check: warns if annotation not on a new single line | yes | - | - |
| 3 | 3.2 | WRONG_MULTIPLE_MODIFIERS_ORDER | Check: if multiple modifiers sequence is in the wrong order | yes | - | - |
| 4 | 4.1.1 | FLOAT_IN_ACCURATE_CALCULATIONS |Checks that floating-point values are not used in arithmetic expressions<br>Fix: no | no | - | - |
petertrr marked this conversation as resolved.
Show resolved Hide resolved