Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
213 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# frozen_string_literal: true | ||
|
||
module RuboCop | ||
module Cop | ||
module Lint | ||
# This cop checks for the presence of precise comparison of floating point numbers. | ||
# | ||
# Floating point values are inherently inaccurate, and comparing them for exact equality | ||
# is almost never the desired semantics. Comparison via the `==/!=` operators checks | ||
# floating-point value representation to be exactly the same, which is very unlikely | ||
# if you perform any arithmetic operations involving precision loss. | ||
# | ||
# @example | ||
# # bad | ||
# x == 0.1 | ||
# x != 0.1 | ||
# | ||
# # good - using BigDecimal | ||
# x.to_d == 0.1.to_d | ||
# | ||
# # good | ||
# (x - 0.1).abs < Float::EPSILON | ||
# | ||
# # good | ||
# tolerance = 0.0001 | ||
# (x - 0.1).abs < tolerance | ||
# | ||
# # Or some other epsilon based type of comparison: | ||
# # https://www.embeddeduse.com/2019/08/26/qt-compare-two-floats/ | ||
# | ||
class FloatComparison < Base | ||
MSG = 'Avoid (in)equality comparisons of floats as they are unreliable.' | ||
|
||
EQUALITY_METHODS = %i[== != eql? equal?].freeze | ||
FLOAT_RETURNING_METHODS = %i[to_f Float fdiv].freeze | ||
FLOAT_INSTANCE_METHODS = %i[@- abs magnitude modulo next_float prev_float quo].to_set.freeze | ||
|
||
def on_send(node) | ||
return unless EQUALITY_METHODS.include?(node.method_name) | ||
|
||
lhs, _method, rhs = *node | ||
add_offense(node) if float?(lhs) || float?(rhs) | ||
end | ||
|
||
private | ||
|
||
def float?(node) | ||
return false unless node | ||
|
||
case node.type | ||
when :float | ||
true | ||
when :send | ||
check_send(node) | ||
when :begin | ||
float?(node.children.first) | ||
else | ||
false | ||
end | ||
end | ||
|
||
# rubocop:disable Metrics/PerceivedComplexity | ||
def check_send(node) | ||
if node.arithmetic_operation? | ||
lhs, _operation, rhs = *node | ||
float?(lhs) || float?(rhs) | ||
elsif FLOAT_RETURNING_METHODS.include?(node.method_name) | ||
true | ||
elsif node.receiver&.float_type? | ||
if FLOAT_INSTANCE_METHODS.include?(node.method_name) | ||
true | ||
else | ||
check_numeric_returning_method(node) | ||
end | ||
end | ||
end | ||
# rubocop:enable Metrics/PerceivedComplexity | ||
|
||
def check_numeric_returning_method(node) | ||
return false unless node.receiver | ||
|
||
case node.method_name | ||
when :angle, :arg, :phase | ||
Float(node.receiver.source).negative? | ||
when :ceil, :floor, :round, :truncate | ||
precision = node.first_argument | ||
precision&.int_type? && Integer(precision.source).positive? | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe RuboCop::Cop::Lint::FloatComparison do | ||
subject(:cop) { described_class.new } | ||
|
||
it 'registers an offense when comparing with float' do | ||
offenses = inspect_source(<<~RUBY) | ||
x == 0.1 | ||
0.1 == x | ||
x != 0.1 | ||
0.1 != x | ||
x.eql?(0.1) | ||
0.1.eql?(x) | ||
RUBY | ||
|
||
expect(offenses.size).to eq(6) | ||
end | ||
|
||
it 'registers an offense when comparing with float returning method' do | ||
offenses = inspect_source(<<~RUBY) | ||
x == Float(1) | ||
x == '0.1'.to_f | ||
x == 1.fdiv(2) | ||
RUBY | ||
|
||
expect(offenses.size).to eq(3) | ||
end | ||
|
||
it 'registers an offense when comparing with arightmetic operator on floats' do | ||
offenses = inspect_source(<<~RUBY) | ||
x == 0.1 + y | ||
x == y + Float('0.1') | ||
x == y + z * (foo(arg) + '0.1'.to_f) | ||
RUBY | ||
|
||
expect(offenses.size).to eq(3) | ||
end | ||
|
||
it 'registers an offense when comparing with method on float receiver' do | ||
expect_offense(<<~RUBY) | ||
x == 0.1.abs | ||
^^^^^^^^^^^^ Avoid (in)equality comparisons of floats as they are unreliable. | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when comparing with float method '\ | ||
'that can return numeric and returns integer' do | ||
expect_no_offenses(<<~RUBY) | ||
x == 1.1.ceil | ||
RUBY | ||
end | ||
|
||
it 'registers an offense when comparing with float method '\ | ||
'that can return numeric and returns float' do | ||
expect_offense(<<~RUBY) | ||
x == 1.1.ceil(1) | ||
^^^^^^^^^^^^^^^^ Avoid (in)equality comparisons of floats as they are unreliable. | ||
RUBY | ||
end | ||
|
||
it 'does not register an offense when comparing with float using epsilon' do | ||
expect_no_offenses(<<~RUBY) | ||
(x - 0.1) < epsilon | ||
RUBY | ||
end | ||
end |