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

Add Style/PercentLiteralDelimiters rule #570

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions spec/ameba/rule/style/percent_literal_delimiters_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
require "../../../spec_helper"

module Ameba::Rule::Style
describe PercentLiteralDelimiters do
subject = PercentLiteralDelimiters.new

it "passes if percent literal delimiters are written correctly" do
expect_no_issues subject, <<-CRYSTAL
%(one two three)
%w[one two three]
%i[one two three]
%r{one(two )?three[!]}
CRYSTAL
end

it "fails if percent literal delimiters are written incorrectly" do
expect_issue subject, <<-CRYSTAL
%[one two three]
# ^{} error: `%`-literals should be delimited by `(` and `)`
%w(one two three)
# ^{} error: `%w`-literals should be delimited by `[` and `]`
%i(one two three)
# ^{} error: `%i`-literals should be delimited by `[` and `]`
%r|one two three|
# ^{} error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
end

it "reports rule, location and message" do
expect_issue subject, <<-CRYSTAL
puts %[one two three]
# ^ error: `%`-literals should be delimited by `(` and `)`
puts %w(one two three)
# ^^ error: `%w`-literals should be delimited by `[` and `]`
CRYSTAL
end

context "properties" do
context "#default_delimiters" do
it "allows setting custom values" do
rule = PercentLiteralDelimiters.new
rule.default_delimiters = "||"
rule.preferred_delimiters = {
"%w" => "{}",
} of String => String?

expect_no_issues rule, <<-CRYSTAL
%w{one two three}
%i|one two three|
CRYSTAL
end

it "allows ignoring default delimiters by setting them to `nil`" do
rule = PercentLiteralDelimiters.new
rule.default_delimiters = nil
rule.preferred_delimiters = {
"%q" => "{}",
} of String => String?

expect_no_issues rule, <<-CRYSTAL
%w(one two three)
%i|one two three|
%r<foo(bar)?>
CRYSTAL

expect_issue rule, <<-CRYSTAL
%q[one two three]
# ^{} error: `%q`-literals should be delimited by `{` and `}`
CRYSTAL
end
end

context "#preferred_delimiters" do
it "allows setting custom values" do
rule = PercentLiteralDelimiters.new
rule.preferred_delimiters = {
"%w" => "()",
"%i" => "||",
} of String => String?

expect_no_issues rule, <<-CRYSTAL
%w(one two three)
%i|one two three|
CRYSTAL
end

it "allows ignoring certain delimiters by setting them to `nil`" do
rule = PercentLiteralDelimiters.new
rule.preferred_delimiters["%r"] = nil

expect_no_issues rule, <<-CRYSTAL
%r[foo(bar)?]
%r{foo(bar)?}
%r<foo(bar)?>
CRYSTAL
end
end

context "#ignore_literals_containing_delimiters?" do
it "ignores different delimiters if enabled" do
rule = PercentLiteralDelimiters.new
rule.ignore_literals_containing_delimiters = true

expect_issue rule, <<-CRYSTAL
%[one two three]
# ^{} error: `%`-literals should be delimited by `(` and `)`
%w(one two three)
# ^{} error: `%w`-literals should be delimited by `[` and `]`
%i(one two three)
# ^{} error: `%i`-literals should be delimited by `[` and `]`
%r<foo[o]>
# ^{} error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL

expect_no_issues rule, <<-CRYSTAL
%[one (two) three]
%w([] []?)
%i([] []?)
%r<foo[o]{1,3}>
CRYSTAL
end

it "ignores different delimiters if disabled" do
rule = PercentLiteralDelimiters.new
rule.ignore_literals_containing_delimiters = false

expect_issue rule, <<-CRYSTAL
%[(one two three)]
# ^{} error: `%`-literals should be delimited by `(` and `)`
%w([] []?)
# ^{} error: `%w`-literals should be delimited by `[` and `]`
%i([] []?)
# ^{} error: `%i`-literals should be delimited by `[` and `]`
%r<foo[o]{1,3}>
# ^{} error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
end
end
end
end
end
14 changes: 7 additions & 7 deletions spec/ameba/tokenizer_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ module Ameba

describe Tokenizer do
describe "#run" do
it_tokenizes %("string"), %w(DELIMITER_START STRING DELIMITER_END EOF)
it_tokenizes %(100), %w(NUMBER EOF)
it_tokenizes %('a'), %w(CHAR EOF)
it_tokenizes %([]), %w([] EOF)
it_tokenizes %([] of String), %w([] SPACE IDENT SPACE CONST EOF)
it_tokenizes %q("str #{3}"), %w(
it_tokenizes %("string"), %w[DELIMITER_START STRING DELIMITER_END EOF]
it_tokenizes %(100), %w[NUMBER EOF]
it_tokenizes %('a'), %w[CHAR EOF]
it_tokenizes %([]), %w[[] EOF]
it_tokenizes %([] of String), %w[[] SPACE IDENT SPACE CONST EOF]
it_tokenizes %q("str #{3}"), %w[
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
)
]

it_tokenizes %(%w[1 2]),
%w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
Expand Down
4 changes: 2 additions & 2 deletions src/ameba/config.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ class Ameba::Config
Path[XDG_CONFIG_HOME] / "ameba/config.yml",
}

DEFAULT_GLOBS = %w(
DEFAULT_GLOBS = %w[
**/*.cr
!lib
)
]

Ameba.ecr_supported? do
DEFAULT_GLOBS << "**/*.ecr"
Expand Down
2 changes: 1 addition & 1 deletion src/ameba/rule/lint/require_parentheses.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module Ameba::Rule::Lint

MSG = "Use parentheses in the method call to avoid confusion about precedence"

ALLOWED_CALL_NAMES = %w{[]? []}
ALLOWED_CALL_NAMES = %w[[]? []]

def test(source, node : Crystal::Call)
return if node.args.empty? ||
Expand Down
82 changes: 82 additions & 0 deletions src/ameba/rule/style/percent_literal_delimiters.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module Ameba::Rule::Style
# A rule that enforces the consistent usage of `%`-literal delimiters.
#
# Specifying `DefaultDelimiters` option will set all preferred delimiters at once. You
# can continue to specify individual preferred delimiters via `PreferredDelimiters`
# setting to override the default. In both cases the delimiters should be specified
# as a string of two characters, or `nil` to ignore a particular `%`-literal / default.
#
# Setting `IgnoreLiteralsContainingDelimiters` to `true` will ignore `%`-literals that
# contain one or both delimiters.
#
# YAML configuration example:
#
# ```
# Style/PercentLiteralDelimiters:
# Enabled: true
# DefaultDelimiters: '()'
# PreferredDelimiters:
# '%w': '[]'
# '%i': '[]'
# '%r': '{}'
# IgnoreLiteralsContainingDelimiters: false
# ```
class PercentLiteralDelimiters < Base
properties do
since_version "1.7.0"
description "Enforces the consistent usage of `%`-literal delimiters"

default_delimiters "()", as: String?
preferred_delimiters({
"%w" => "[]",
"%i" => "[]",
"%r" => "{}",
} of String => String?)
ignore_literals_containing_delimiters false
end

MSG = "`%s`-literals should be delimited by `%s` and `%s`"

# ameba:disable Metrics/CyclomaticComplexity
def test(source)
start_token = literal = delimiters = nil

Tokenizer.new(source).run do |token|
case token.type
when .string_array_start?, .symbol_array_start?, .delimiter_start?
if literal = token.raw.match(/^(%\w?)\W/i).try &.[1]
start_token = token.dup

delimiters =
preferred_delimiters.fetch(literal) { default_delimiters }

# `nil` means that the check should be skipped for this literal
unless delimiters
start_token = literal = delimiters = nil
end
end
when .string?
if (_delimiters = delimiters) && ignore_literals_containing_delimiters?
# literal contains one or both delimiters
if token.raw[_delimiters[0]]? || token.raw[_delimiters[1]]?
start_token = literal = delimiters = nil
end
end
when .string_array_end?, .delimiter_end?
if (_start = start_token) && (_delimiters = delimiters) && (_literal = literal)
unless _start.delimiter_state.nest == _delimiters[0] &&
_start.delimiter_state.end == _delimiters[1]
token_location = {
_start.location,
_start.location.adjust(column_number: _literal.size - 1),
}
issue_for *token_location,
MSG % {_literal, _delimiters[0], _delimiters[1]}
end
start_token = literal = delimiters = nil
end
end
end
end
end
end