diff --git a/spec/ameba/rule/style/percent_literal_delimiters_spec.cr b/spec/ameba/rule/style/percent_literal_delimiters_spec.cr new file mode 100644 index 000000000..72b2391ba --- /dev/null +++ b/spec/ameba/rule/style/percent_literal_delimiters_spec.cr @@ -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 + 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 + 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 + # ^{} error: `%r`-literals should be delimited by `{` and `}` + CRYSTAL + + expect_no_issues rule, <<-CRYSTAL + %[one (two) three] + %w([] []?) + %i([] []?) + %r + 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 + # ^{} error: `%r`-literals should be delimited by `{` and `}` + CRYSTAL + end + end + end + end +end diff --git a/spec/ameba/tokenizer_spec.cr b/spec/ameba/tokenizer_spec.cr index 1c7772270..0d4d633e6 100644 --- a/spec/ameba/tokenizer_spec.cr +++ b/spec/ameba/tokenizer_spec.cr @@ -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] diff --git a/src/ameba/config.cr b/src/ameba/config.cr index 0be608cf7..1a470c522 100644 --- a/src/ameba/config.cr +++ b/src/ameba/config.cr @@ -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" diff --git a/src/ameba/rule/lint/require_parentheses.cr b/src/ameba/rule/lint/require_parentheses.cr index c6bcf3012..aa06d19cb 100644 --- a/src/ameba/rule/lint/require_parentheses.cr +++ b/src/ameba/rule/lint/require_parentheses.cr @@ -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? || diff --git a/src/ameba/rule/style/percent_literal_delimiters.cr b/src/ameba/rule/style/percent_literal_delimiters.cr new file mode 100644 index 000000000..6e4dcaab4 --- /dev/null +++ b/src/ameba/rule/style/percent_literal_delimiters.cr @@ -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