Merge pull request #317 from crystal-ameba/Sija/further-refinements

This commit is contained in:
Sijawusz Pur Rahnama 2022-12-11 13:25:07 +01:00 committed by GitHub
commit 4533e52aa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 130 additions and 131 deletions

View file

@ -19,21 +19,21 @@
- [About](#about) - [About](#about)
- [Usage](#usage) - [Usage](#usage)
* [Watch a tutorial](#watch-a-tutorial) - [Watch a tutorial](#watch-a-tutorial)
* [Autocorrection](#autocorrection) - [Autocorrection](#autocorrection)
* [Explain issues](#explain-issues) - [Explain issues](#explain-issues)
* [Run in parallel](#run-in-parallel) - [Run in parallel](#run-in-parallel)
- [Installation](#installation) - [Installation](#installation)
* [As a project dependency:](#as-a-project-dependency) - [As a project dependency:](#as-a-project-dependency)
* [OS X](#os-x) - [OS X](#os-x)
* [Docker](#docker) - [Docker](#docker)
* [From sources](#from-sources) - [From sources](#from-sources)
- [Configuration](#configuration) - [Configuration](#configuration)
* [Sources](#sources) - [Sources](#sources)
* [Rules](#rules) - [Rules](#rules)
* [Inline disabling](#inline-disabling) - [Inline disabling](#inline-disabling)
- [Editors & integrations](#editors--integrations) - [Editors \& integrations](#editors--integrations)
- [Credits & inspirations](#credits--inspirations) - [Credits \& inspirations](#credits--inspirations)
- [Contributors](#contributors) - [Contributors](#contributors)
## About ## About
@ -201,8 +201,8 @@ In this example we define default globs and exclude `src/compiler` folder:
``` yaml ``` yaml
Globs: Globs:
- **/*.cr - "**/*.cr"
- !lib - "!lib"
Excluded: Excluded:
- src/compiler - src/compiler
@ -245,18 +245,17 @@ One or more rules or one or more group of rules can be disabled using inline dir
time = Time.epoch(1483859302) time = Time.epoch(1483859302)
time = Time.epoch(1483859302) # ameba:disable Style/LargeNumbers, Lint/UselessAssign time = Time.epoch(1483859302) # ameba:disable Style/LargeNumbers, Lint/UselessAssign
time = Time.epoch(1483859302) # ameba:disable Style, Lint time = Time.epoch(1483859302) # ameba:disable Style, Lint
``` ```
## Editors & integrations ## Editors & integrations
* Vim: [vim-crystal](https://github.com/rhysd/vim-crystal), [Ale](https://github.com/w0rp/ale) - Vim: [vim-crystal](https://github.com/rhysd/vim-crystal), [Ale](https://github.com/w0rp/ale)
* Emacs: [ameba.el](https://github.com/crystal-ameba/ameba.el) - Emacs: [ameba.el](https://github.com/crystal-ameba/ameba.el)
* Sublime Text: [Sublime Linter Ameba](https://github.com/epergo/SublimeLinter-contrib-ameba) - Sublime Text: [Sublime Linter Ameba](https://github.com/epergo/SublimeLinter-contrib-ameba)
* VSCode: [vscode-crystal-ameba](https://github.com/crystal-ameba/vscode-crystal-ameba) - VSCode: [vscode-crystal-ameba](https://github.com/crystal-ameba/vscode-crystal-ameba)
* Codacy: [codacy-ameba](https://github.com/codacy/codacy-ameba) - Codacy: [codacy-ameba](https://github.com/codacy/codacy-ameba)
* GitHub Actions: [github-action](https://github.com/crystal-ameba/github-action) - GitHub Actions: [github-action](https://github.com/crystal-ameba/github-action)
## Credits & inspirations ## Credits & inspirations

View file

@ -39,7 +39,7 @@ module Ameba::Formatter
describe "#context" do describe "#context" do
it "returns correct pre/post context lines" do it "returns correct pre/post context lines" do
source = Source.new <<-EOF source = Source.new <<-CRYSTAL
# pre:1 # pre:1
# pre:2 # pre:2
# pre:3 # pre:3
@ -51,7 +51,7 @@ module Ameba::Formatter
# post:3 # post:3
# post:4 # post:4
# post:5 # post:5
EOF CRYSTAL
subject.context(source.lines, lineno: 6, context_lines: 3) subject.context(source.lines, lineno: 6, context_lines: 3)
.should eq({<<-PRE.lines, <<-POST.lines .should eq({<<-PRE.lines, <<-POST.lines
@ -69,24 +69,24 @@ module Ameba::Formatter
describe "#affected_code" do describe "#affected_code" do
it "returns nil if there is no such a line number" do it "returns nil if there is no such a line number" do
code = <<-EOF code = <<-CRYSTAL
a = 1 a = 1
EOF CRYSTAL
location = Crystal::Location.new("filename", 2, 1) location = Crystal::Location.new("filename", 2, 1)
subject.affected_code(code, location).should be_nil subject.affected_code(code, location).should be_nil
end end
it "returns correct line if it is found" do it "returns correct line if it is found" do
code = <<-EOF code = <<-CRYSTAL
a = 1 a = 1
EOF CRYSTAL
location = Crystal::Location.new("filename", 1, 1) location = Crystal::Location.new("filename", 1, 1)
subject.deansify(subject.affected_code(code, location)) subject.deansify(subject.affected_code(code, location))
.should eq "> a = 1\n ^\n" .should eq "> a = 1\n ^\n"
end end
it "returns correct line if it is found" do it "returns correct line if it is found" do
code = <<-EOF code = <<-CRYSTAL
# pre:1 # pre:1
# pre:2 # pre:2
# pre:3 # pre:3
@ -98,7 +98,7 @@ module Ameba::Formatter
# post:3 # post:3
# post:4 # post:4
# post:5 # post:5
EOF CRYSTAL
location = Crystal::Location.new("filename", 6, 1) location = Crystal::Location.new("filename", 6, 1)
subject.deansify(subject.affected_code(code, location, context_lines: 3)) subject.deansify(subject.affected_code(code, location, context_lines: 3))

View file

@ -41,24 +41,20 @@ module Ameba::Rule::Lint
CRYSTAL CRYSTAL
end end
it "reports if there is a static path comparison evaluating to false" do
expect_issue subject, <<-CRYSTAL
String == Nil
# ^^^^^^^^^^^ error: Comparison always evaluates to false
CRYSTAL
end
context "macro" do context "macro" do
pending "reports in macro scope" do it "reports in macro scope" do
expect_issue subject, <<-CRYSTAL expect_issue subject, <<-CRYSTAL
{{ "foo" == "foo" }} {{ "foo" == "foo" }}
# ^^^^^^^^^^^^^^ error: Comparison always evaluates to true # ^^^^^^^^^^^^^^ error: Comparison always evaluates to true
CRYSTAL CRYSTAL
end end
it "passes for free variables comparisons in macro scope" do it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
{{ T == Nil }} {{ "foo" == foo }}
{{ "foo" != foo }}
{% foo == "foo" %}
{% foo != "foo" %}
CRYSTAL CRYSTAL
end end
end end

View file

@ -18,6 +18,13 @@ module Ameba::Rule::Lint
CRYSTAL CRYSTAL
end end
it "reports if there is a `not_nil!` call in the middle of the call-chain" do
expect_issue subject, <<-CRYSTAL
(1..3).first?.not_nil!.to_s
# ^^^^^^^^ error: Avoid using `not_nil!`
CRYSTAL
end
context "macro" do context "macro" do
it "doesn't report in macro scope" do it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL

View file

@ -2,7 +2,7 @@ require "../../../spec_helper"
module Ameba::Rule::Metrics module Ameba::Rule::Metrics
subject = CyclomaticComplexity.new subject = CyclomaticComplexity.new
complex_method = <<-CODE complex_method = <<-CRYSTAL
def hello(a, b, c) def hello(a, b, c)
if a && b && c if a && b && c
begin begin
@ -15,7 +15,7 @@ module Ameba::Rule::Metrics
end end
end end
end end
CODE CRYSTAL
describe CyclomaticComplexity do describe CyclomaticComplexity do
it "passes for empty methods" do it "passes for empty methods" do

View file

@ -14,17 +14,25 @@ module Ameba::Rule::Style
end end
it "reports if there is a call to is_a?(Nil) without receiver" do it "reports if there is a call to is_a?(Nil) without receiver" do
expect_issue subject, <<-CRYSTAL source = expect_issue subject, <<-CRYSTAL
a = is_a?(Nil) a = is_a?(Nil)
# ^^^ error: Use `nil?` instead of `is_a?(Nil)` # ^^^ error: Use `nil?` instead of `is_a?(Nil)`
CRYSTAL CRYSTAL
expect_correction source, <<-CRYSTAL
a = self.nil?
CRYSTAL
end end
it "reports if there is a call to is_a?(Nil) with receiver" do it "reports if there is a call to is_a?(Nil) with receiver" do
expect_issue subject, <<-CRYSTAL source = expect_issue subject, <<-CRYSTAL
a.is_a?(Nil) a.is_a?(Nil)
# ^^^ error: Use `nil?` instead of `is_a?(Nil)` # ^^^ error: Use `nil?` instead of `is_a?(Nil)`
CRYSTAL CRYSTAL
expect_correction source, <<-CRYSTAL
a.nil?
CRYSTAL
end end
it "reports rule, location and message" do it "reports rule, location and message" do

View file

@ -32,7 +32,7 @@ module Ameba::AST
def initialize(@node, @parent) def initialize(@node, @parent)
end end
# Returns true if current branch is in a loop, false - otherwise. # Returns `true` if current branch is in a loop, `false` - otherwise.
# For example, this branch is in a loop: # For example, this branch is in a loop:
# #
# ``` # ```

View file

@ -35,7 +35,8 @@ module Ameba::AST
def initialize(@node, @parent = nil) def initialize(@node, @parent = nil)
end end
# Returns true if this node or one of the parent branchables is a loop, false otherwise. # Returns `true` if this node or one of the parent branchables is a loop,
# `false` otherwise.
def loop? def loop?
loop?(node) || !!parent.try(&.loop?) loop?(node) || !!parent.try(&.loop?)
end end

View file

@ -134,7 +134,7 @@ module Ameba::AST
node.is_a?(Crystal::CStructOrUnionDef) node.is_a?(Crystal::CStructOrUnionDef)
end end
# Returns true if current scope (or any of inner scopes) references variable, # Returns `true` if current scope (or any of inner scopes) references variable,
# `false` otherwise. # `false` otherwise.
def references?(variable : Variable, check_inner_scopes = true) def references?(variable : Variable, check_inner_scopes = true)
variable.references.any? do |reference| variable.references.any? do |reference|
@ -153,7 +153,7 @@ module Ameba::AST
outer_scope.nil? outer_scope.nil?
end end
# Returns true if var is an argument in current scope, `false` otherwise. # Returns `true` if var is an argument in current scope, `false` otherwise.
def arg?(var) def arg?(var)
case current_node = node case current_node = node
when Crystal::Def when Crystal::Def

View file

@ -4,7 +4,7 @@ module Ameba::AST::Util
# #
# 1. is *node* a literal? # 1. is *node* a literal?
# 2. can *node* be proven static? # 2. can *node* be proven static?
protected def literal_kind?(node, include_paths = false) : {Bool, Bool} protected def literal_kind?(node) : {Bool, Bool}
case node case node
when Crystal::NilLiteral, when Crystal::NilLiteral,
Crystal::BoolLiteral, Crystal::BoolLiteral,
@ -17,44 +17,42 @@ module Ameba::AST::Util
Crystal::MacroLiteral Crystal::MacroLiteral
{true, true} {true, true}
when Crystal::RangeLiteral when Crystal::RangeLiteral
{true, static_literal?(node.from, include_paths) && {true, static_literal?(node.from) &&
static_literal?(node.to, include_paths)} static_literal?(node.to)}
when Crystal::ArrayLiteral, when Crystal::ArrayLiteral,
Crystal::TupleLiteral Crystal::TupleLiteral
{true, node.elements.all? do |el| {true, node.elements.all? do |el|
static_literal?(el, include_paths) static_literal?(el)
end} end}
when Crystal::HashLiteral when Crystal::HashLiteral
{true, node.entries.all? do |entry| {true, node.entries.all? do |entry|
static_literal?(entry.key, include_paths) && static_literal?(entry.key) &&
static_literal?(entry.value, include_paths) static_literal?(entry.value)
end} end}
when Crystal::NamedTupleLiteral when Crystal::NamedTupleLiteral
{true, node.entries.all? do |entry| {true, node.entries.all? do |entry|
static_literal?(entry.value, include_paths) static_literal?(entry.value)
end} end}
when Crystal::Path
{include_paths, true}
else else
{false, false} {false, false}
end end
end end
# Returns `true` if current `node` is a static literal, `false` otherwise. # Returns `true` if current `node` is a static literal, `false` otherwise.
def static_literal?(node, include_paths = false) : Bool def static_literal?(node) : Bool
is_literal, is_static = literal_kind?(node, include_paths) is_literal, is_static = literal_kind?(node)
is_literal && is_static is_literal && is_static
end end
# Returns `true` if current `node` is a dynamic literal, `false` otherwise. # Returns `true` if current `node` is a dynamic literal, `false` otherwise.
def dynamic_literal?(node, include_paths = false) : Bool def dynamic_literal?(node) : Bool
is_literal, is_static = literal_kind?(node, include_paths) is_literal, is_static = literal_kind?(node)
is_literal && !is_static is_literal && !is_static
end end
# Returns `true` if current `node` is a literal, `false` otherwise. # Returns `true` if current `node` is a literal, `false` otherwise.
def literal?(node, include_paths = false) : Bool def literal?(node) : Bool
is_literal, _ = literal_kind?(node, include_paths) is_literal, _ = literal_kind?(node)
is_literal is_literal
end end
@ -93,8 +91,8 @@ module Ameba::AST::Util
end end
return if last_line.size < end_column + 1 return if last_line.size < end_column + 1
node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "")
node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "")
node_lines.join('\n') node_lines.join('\n')
end end

View file

@ -31,7 +31,7 @@ module Ameba::AST
def initialize(@node, @variable) def initialize(@node, @variable)
end end
# Returns true if the name starts with '_', false if not. # Returns `true` if the name starts with '_', `false` if not.
def ignored? def ignored?
name.starts_with? '_' name.starts_with? '_'
end end

View file

@ -41,7 +41,7 @@ module Ameba::AST
@variable.referenced? && !!@branch.try(&.in_loop?) @variable.referenced? && !!@branch.try(&.in_loop?)
end end
# Returns true if this assignment is an op assign, false if not. # Returns `true` if this assignment is an op assign, `false` if not.
# For example, this is an op assign: # For example, this is an op assign:
# #
# ``` # ```
@ -51,7 +51,7 @@ module Ameba::AST
node.is_a?(Crystal::OpAssign) node.is_a?(Crystal::OpAssign)
end end
# Returns true if this assignment is in a branch, false if not. # Returns `true` if this assignment is in a branch, `false` if not.
# For example, this assignment is in a branch: # For example, this assignment is in a branch:
# #
# ``` # ```

View file

@ -174,7 +174,7 @@ module Ameba::AST
node.accept self node.accept self
end end
# @[AlwaysInline] @[AlwaysInline]
private def includes_reference?(val) private def includes_reference?(val)
val.to_s.includes?(@reference) val.to_s.includes?(@reference)
end end

View file

@ -20,7 +20,7 @@ module Ameba::AST
end end
# A main visit method that accepts `Crystal::ASTNode`. # A main visit method that accepts `Crystal::ASTNode`.
# Returns true meaning all child nodes will be traversed. # Returns `true`, meaning all child nodes will be traversed.
def visit(node : Crystal::ASTNode) def visit(node : Crystal::ASTNode)
true true
end end

View file

@ -54,7 +54,7 @@ class Ameba::Config
# ``` # ```
property excluded : Array(String) property excluded : Array(String)
# Returns true if correctable issues should be autocorrected. # Returns `true` if correctable issues should be autocorrected.
property? autocorrect = false property? autocorrect = false
@rule_groups : Hash(String, Array(Rule::Base)) @rule_groups : Hash(String, Array(Rule::Base))

View file

@ -10,8 +10,8 @@ module Ameba
Enable Enable
end end
# Returns true if current location is disabled for a particular rule, # Returns `true` if current location is disabled for a particular rule,
# false otherwise. # `false` otherwise.
# #
# Location is disabled in two cases: # Location is disabled in two cases:
# 1. The line of the location ends with a comment directive. # 1. The line of the location ends with a comment directive.
@ -74,7 +74,7 @@ module Ameba
} }
end end
# Returns true if the line at the given `line_number` is a comment. # Returns `true` if the line at the given `line_number` is a comment.
def comment?(line_number : Int32) def comment?(line_number : Int32)
return unless line = lines[line_number]? return unless line = lines[line_number]?
comment?(line) comment?(line)

View file

@ -21,8 +21,8 @@ module Ameba
# :ditto: # :ditto:
def add_issue(rule, def add_issue(rule,
location : Crystal::Location, location : Crystal::Location?,
end_location : Crystal::Location, end_location : Crystal::Location?,
message : String, message : String,
status : Issue::Status? = nil, status : Issue::Status? = nil,
&block : Source::Corrector ->) : Issue &block : Source::Corrector ->) : Issue

View file

@ -94,7 +94,7 @@ module Ameba::Rule
end end
end end
# Returns true if this rule is special and behaves differently than # Returns `true` if this rule is special and behaves differently than
# usual rules. # usual rules.
# #
# ``` # ```

View file

@ -17,7 +17,6 @@ module Ameba::Rule::Lint
# #
# And it should be written as this: # And it should be written as this:
# #
#
# ``` # ```
# def some_method # def some_method
# do_some_stuff # do_some_stuff

View file

@ -47,9 +47,7 @@ module Ameba::Rule::Lint
MSG = "Empty loop detected" MSG = "Empty loop detected"
def test(source, node : Crystal::Call) def test(source, node : Crystal::Call)
return unless loop?(node) check_node(source, node, node.block) if loop?(node)
check_node(source, node, node.block)
end end
def test(source, node : Crystal::While | Crystal::Until) def test(source, node : Crystal::While | Crystal::Until)
@ -58,7 +56,9 @@ module Ameba::Rule::Lint
private def check_node(source, node, loop_body) private def check_node(source, node, loop_body)
body = loop_body.is_a?(Crystal::Block) ? loop_body.body : loop_body body = loop_body.is_a?(Crystal::Block) ? loop_body.body : loop_body
issue_for node, MSG if body.nil? || body.nop? return unless body.nil? || body.nop?
issue_for node, MSG
end end
end end
end end

View file

@ -35,7 +35,7 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until) def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until)
return unless (cond = node.cond).is_a?(Crystal::Assign) return unless (cond = node.cond).is_a?(Crystal::Assign)
return unless literal?(cond.value, include_paths: true) return unless literal?(cond.value)
issue_for cond, MSG issue_for cond, MSG
end end

View file

@ -7,6 +7,7 @@ module Ameba::Rule::Lint
# replaced with either the body of the construct, or deleted entirely. # replaced with either the body of the construct, or deleted entirely.
# #
# This is considered invalid: # This is considered invalid:
#
# ``` # ```
# if "something" # if "something"
# :ok # :ok
@ -29,12 +30,8 @@ module Ameba::Rule::Lint
MSG = "Literal value found in conditional" MSG = "Literal value found in conditional"
def check_node(source, node) def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case)
issue_for node, MSG if static_literal?(node.cond) issue_for node, MSG if static_literal?(node.cond)
end end
def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case)
check_node source, node
end
end end
end end

View file

@ -4,7 +4,6 @@ module Ameba::Rule::Lint
# They usually have the same result - except for non-primitive # They usually have the same result - except for non-primitive
# types like containers, range or regex. # types like containers, range or regex.
# #
#
# For example, this will be always false: # For example, this will be always false:
# #
# ``` # ```
@ -29,28 +28,12 @@ module Ameba::Rule::Lint
MSG = "Comparison always evaluates to %s" MSG = "Comparison always evaluates to %s"
MSG_LIKELY = "Comparison most likely evaluates to %s" MSG_LIKELY = "Comparison most likely evaluates to %s"
# Edge-case: `{{ T == Nil }}`
#
# Current implementation just skips all macro contexts,
# regardless of the free variable being present.
#
# Ideally we should only check whether either of the sides
# is a free var
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::Call) def test(source, node : Crystal::Call)
return unless node.name.in?(OP_NAMES) return unless node.name.in?(OP_NAMES)
return unless (obj = node.obj) && (arg = node.args.first?) return unless (obj = node.obj) && (arg = node.args.first?)
obj_is_literal, obj_is_static = literal_kind?(obj, include_paths: true) obj_is_literal, obj_is_static = literal_kind?(obj)
arg_is_literal, arg_is_static = literal_kind?(arg, include_paths: true) arg_is_literal, arg_is_static = literal_kind?(arg)
return unless obj_is_literal && arg_is_literal return unless obj_is_literal && arg_is_literal

View file

@ -32,10 +32,8 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::Call) def test(source, node : Crystal::Call)
return unless node.name == "rand" && return unless node.name == "rand" &&
node.args.size == 1 && node.args.size == 1 &&
(arg = node.args.first) && (arg = node.args.first).is_a?(Crystal::NumberLiteral) &&
arg.is_a?(Crystal::NumberLiteral) && arg.value.in?("0", "1")
(value = arg.value) &&
value.in?("0", "1")
issue_for node, MSG % node issue_for node, MSG % node
end end

View file

@ -17,7 +17,7 @@ module Ameba::Rule::Lint
# YAML configuration example: # YAML configuration example:
# #
# ``` # ```
# Lint/RedundantStringCoersion # Lint/RedundantStringCoercion
# Enabled: true # Enabled: true
# ``` # ```
class RedundantStringCoercion < Base class RedundantStringCoercion < Base

View file

@ -42,7 +42,6 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::ExceptionHandler) def test(source, node : Crystal::ExceptionHandler)
rescues = node.rescues rescues = node.rescues
return if rescues.nil? return if rescues.nil?
shadowed(rescues).each do |path| shadowed(rescues).each do |path|

View file

@ -63,8 +63,9 @@ module Ameba::Rule::Lint
return unless node.block return unless node.block
arg = node.named_args.try &.find(&.name.== "focus") arg = node.named_args.try &.find(&.name.== "focus")
return unless arg
issue_for arg, MSG if arg issue_for arg, MSG
end end
end end
end end

View file

@ -7,6 +7,7 @@ module Ameba::Rule::Lint
# a + b # a + b
# end # end
# ``` # ```
#
# and should be written as: # and should be written as:
# #
# ``` # ```

View file

@ -41,6 +41,7 @@ module Ameba::Rule::Performance
return unless node.name == ANY_NAME return unless node.name == ANY_NAME
return unless node.block.nil? && node.args.empty? return unless node.block.nil? && node.args.empty?
return unless node.obj return unless node.obj
return unless location = node.location return unless location = node.location
return unless name_location = node.name_location return unless name_location = node.name_location
return unless end_location = name_end_location(node) return unless end_location = name_end_location(node)

View file

@ -68,12 +68,13 @@ module Ameba::Rule::Performance
end end
def test(source, node : Crystal::Call) def test(source, node : Crystal::Call)
return unless location = node.name_location
return unless end_location = name_end_location(node)
return unless (obj = node.obj).is_a?(Crystal::Call) return unless (obj = node.obj).is_a?(Crystal::Call)
return unless node.name.in?(call_names) return unless node.name.in?(call_names)
return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES) return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES)
return unless location = node.name_location
return unless end_location = name_end_location(node)
issue_for location, end_location, MSG % {node.name, obj.name} do |corrector| issue_for location, end_location, MSG % {node.name, obj.name} do |corrector|
corrector.insert_after(end_location, '!') corrector.insert_after(end_location, '!')
end end

View file

@ -51,7 +51,9 @@ module Ameba::Rule::Performance
return unless obj.name.in?(filter_names) return unless obj.name.in?(filter_names)
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
issue_for obj.name_location, node.name_end_location,
message % {obj.name, node.name}
end end
end end
end end

View file

@ -34,7 +34,9 @@ module Ameba::Rule::Style
const = node.const const = node.const
return unless path_named?(const, "Nil") return unless path_named?(const, "Nil")
issue_for const, MSG issue_for const, MSG do |corrector|
corrector.replace(node, "#{node.obj}.nil?")
end
end end
end end
end end

View file

@ -59,20 +59,20 @@ module Ameba::Rule::Style
private def allowed?(_sign, value, fraction, _suffix) private def allowed?(_sign, value, fraction, _suffix)
return true if fraction && fraction.size > 3 return true if fraction && fraction.size > 3
digits = value.chars.select(&.number?) digits = value.chars.select!(&.number?)
digits.size >= int_min_digits digits.size >= int_min_digits
end end
private def underscored(sign, value, fraction, suffix) private def underscored(sign, value, fraction, suffix)
value = slice_digits(value.reverse).reverse value = slice_digits(value.reverse).reverse
fraction = "." + slice_digits(fraction) if fraction fraction = ".#{slice_digits(fraction)}" if fraction
"#{sign}#{value}#{fraction}#{suffix}" "#{sign}#{value}#{fraction}#{suffix}"
end end
private def slice_digits(value, by = 3) private def slice_digits(value, by = 3)
%w[].tap do |slices| %w[].tap do |slices|
value.chars.reject(&.== '_').each_slice(by) do |slice| value.chars.reject!(&.== '_').each_slice(by) do |slice|
slices << slice.join slices << slice.join
end end
end.join('_') end.join('_')

View file

@ -48,6 +48,7 @@ module Ameba::Rule::Style
def test(source, node : Crystal::Def) def test(source, node : Crystal::Def)
return if (expected = node.name.underscore) == node.name return if (expected = node.name.underscore) == node.name
return unless location = name_location(node) return unless location = name_location(node)
return unless end_location = name_end_location(node) return unless end_location = name_end_location(node)

View file

@ -188,9 +188,6 @@ module Ameba::Rule::Style
# ameba:disable Metrics/CyclomaticComplexity # ameba:disable Metrics/CyclomaticComplexity
protected def issue_for_valid(source, call : Crystal::Call, block : Crystal::Block, body : Crystal::Call) protected def issue_for_valid(source, call : Crystal::Call, block : Crystal::Block, body : Crystal::Call)
return unless location = call.name_location
return unless end_location = block.end_location
return if exclude_calls_with_block? && body.block return if exclude_calls_with_block? && body.block
return if exclude_multiple_line_blocks? && !same_location_lines?(call, body) return if exclude_multiple_line_blocks? && !same_location_lines?(call, body)
return if exclude_prefix_operators? && prefix_operator?(body) return if exclude_prefix_operators? && prefix_operator?(body)
@ -203,6 +200,9 @@ module Ameba::Rule::Style
return unless valid_line_length?(call, call_code) return unless valid_line_length?(call, call_code)
return unless valid_length?(call_code) return unless valid_length?(call_code)
return unless location = call.name_location
return unless end_location = block.end_location
if call_code.includes?("{...}") if call_code.includes?("{...}")
issue_for location, end_location, MSG % call_code issue_for location, end_location, MSG % call_code
else else

View file

@ -34,6 +34,7 @@ module Ameba::Rule::Style
def test(source, node : Crystal::While) def test(source, node : Crystal::While)
return unless node.cond.true_literal? return unless node.cond.true_literal?
return unless location = node.location return unless location = node.location
return unless end_location = node.cond.end_location return unless end_location = node.cond.end_location

View file

@ -46,7 +46,7 @@ module Ameba
# Checks for unneeded disable directives. Always inspects a source last # Checks for unneeded disable directives. Always inspects a source last
@unneeded_disable_directive_rule : Rule::Base? @unneeded_disable_directive_rule : Rule::Base?
# Returns true if correctable issues should be autocorrected. # Returns `true` if correctable issues should be autocorrected.
private getter? autocorrect : Bool private getter? autocorrect : Bool
# Instantiates a runner using a `config`. # Instantiates a runner using a `config`.
@ -162,7 +162,7 @@ module Ameba
end end
# Indicates whether the last inspection successful or not. # Indicates whether the last inspection successful or not.
# It returns true if no issues matching severity in sources found, false otherwise. # It returns `true` if no issues matching severity in sources found, `false` otherwise.
# #
# ``` # ```
# runner = Ameba::Runner.new config # runner = Ameba::Runner.new config

View file

@ -65,7 +65,7 @@ class Ameba::Source
@action_root = Rewriter::Action.new(0, code.size) @action_root = Rewriter::Action.new(0, code.size)
end end
# Returns true if no (non trivial) update has been recorded # Returns `true` if no (non trivial) update has been recorded
def empty? def empty?
@action_root.empty? @action_root.empty?
end end

View file

@ -70,7 +70,7 @@ class Ameba::Source::Rewriter
end end
protected def place_in_hierarchy(action) protected def place_in_hierarchy(action)
family = analyse_hierarchy(action) family = analyze_hierarchy(action)
sibling_left, sibling_right = family[:sibling_left], family[:sibling_right] sibling_left, sibling_right = family[:sibling_left], family[:sibling_right]
if fusible = family[:fusible] if fusible = family[:fusible]
@ -126,7 +126,7 @@ class Ameba::Source::Rewriter
# In case a child has equal range to *action*, it is returned as `:parent` # In case a child has equal range to *action*, it is returned as `:parent`
# #
# Reminder: an empty range 1...1 is considered disjoint from 1...10 # Reminder: an empty range 1...1 is considered disjoint from 1...10
protected def analyse_hierarchy(action) # ameba:disable Metrics/CyclomaticComplexity protected def analyze_hierarchy(action) # ameba:disable Metrics/CyclomaticComplexity
# left_index is the index of the first child that isn't completely to the left of action # left_index is the index of the first child that isn't completely to the left of action
left_index = bsearch_child_index { |child| child.end_pos > action.begin_pos } left_index = bsearch_child_index { |child| child.end_pos > action.begin_pos }
# right_index is the index of the first child that is completely on the right of action # right_index is the index of the first child that is completely on the right of action
@ -147,6 +147,7 @@ class Ameba::Source::Rewriter
else else
overlap_left = @children[left_index].begin_pos <=> action.begin_pos overlap_left = @children[left_index].begin_pos <=> action.begin_pos
overlap_right = @children[right_index - 1].end_pos <=> action.end_pos overlap_right = @children[right_index - 1].end_pos <=> action.end_pos
raise "Unable to compare begin pos" if overlap_left.nil? raise "Unable to compare begin pos" if overlap_left.nil?
raise "Unable to compare end pos" if overlap_right.nil? raise "Unable to compare end pos" if overlap_right.nil?

View file

@ -160,8 +160,10 @@ module Ameba::Spec::ExpectIssue
file = __FILE__, file = __FILE__,
line = __LINE__) line = __LINE__)
lines = code.split('\n') # must preserve trailing newline lines = code.split('\n') # must preserve trailing newline
_, actual_annotations = actual_annotations(rules, code, path, lines) _, actual_annotations = actual_annotations(rules, code, path, lines)
return if actual_annotations.to_s == code return if actual_annotations.to_s == code
fail <<-MSG, file, line fail <<-MSG, file, line
Expected no issues, but got: Expected no issues, but got:
@ -182,9 +184,10 @@ module Ameba::Spec::ExpectIssue
private def format_issue(code, **replacements) private def format_issue(code, **replacements)
replacements.each do |keyword, value| replacements.each do |keyword, value|
value = value.to_s value = value.to_s
code = code.gsub("%{#{keyword}}", value) code = code
code = code.gsub("^{#{keyword}}", "^" * value.size) .gsub("%{#{keyword}}", value)
code = code.gsub("_{#{keyword}}", " " * value.size) .gsub("^{#{keyword}}", "^" * value.size)
.gsub("_{#{keyword}}", " " * value.size)
end end
code code
end end