rubocop-crystal 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbaf055e01ff70c7c189f9b6a73c94bf6ffbc50d7c0a335103177b5b8b52fb4a
4
- data.tar.gz: ff97d8c93dd042f6dc9288a4adc97dc975c953fda4cc3e4c4af3b7ca7b8fb795
3
+ metadata.gz: 3e7cede16a18df18a0021747a7a5305d1503af8229d0a2790242b3b4ac5e27c2
4
+ data.tar.gz: 2373953ed41151d8a00749f28db4ac84ff144db91e02413b50e71f4d97c5627b
5
5
  SHA512:
6
- metadata.gz: 479c9f1a1c18e2147ebbbc22e8c4aa77d683e5601653d3ddbfa565ba7bee08d94aef3b92e1fabe41afcd5250a529a0a36672623db22a4206e840a40176d7fa4e
7
- data.tar.gz: 262807664ebcfa6e7ae82af586e2c1535378184bfc20cb722932d62a6e6f75465acbd6781b79720a5cfec94460dea4ed31d55080f5192b1203b23c15ccf5b34a
6
+ metadata.gz: e33199a6a2a74eb3e7739f0b2355b58e65b89d6bcc9001c4c62e246eea89fa7cb098dfb274da12b7dac220f90de5d672d94f2abb27d078c8a2ec273f8b81f990
7
+ data.tar.gz: bccb8bb8fba84d6b2208cb578bcbcc7e7a3c6c2202cee707e8fe511018ff128cb4dd6113aefd11912df78a9971183ea8497fe344b684002aa3144279756732c4
data/.rubocop.yml CHANGED
@@ -15,6 +15,7 @@ Gemspec/RequiredRubyVersion:
15
15
 
16
16
  InternalAffairs/OnSendWithoutOnCSend:
17
17
  Exclude:
18
+ - 'lib/rubocop/cop/crystal/require_at_top_level.rb'
18
19
  - 'lib/rubocop/cop/crystal/require_relative.rb'
19
20
 
20
21
  Layout/LineLength:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.0.4 (2025-09-21)
2
+
3
+ ### New features
4
+
5
+ * Add Crystal/EnumerableReduce cop ([@zopolis4][])
6
+ * Add Crystal/EnumerableSize cop ([@zopolis4][])
7
+ * Add Crystal/FileReadLines cop ([@zopolis4][])
8
+ * Add Crystal/MethodReturningChar cop ([@zopolis4][])
9
+ * Add Crystal/RequireAtTopLevel cop ([@zopolis4][])
10
+
11
+ ### Changes
12
+
13
+ * Add files ending in .cr to the list of files to be checked in the default config ([@zopolis4][])
14
+
1
15
  ## 0.0.3 (2025-04-28)
2
16
 
3
17
  ### New features
data/README.md CHANGED
@@ -10,7 +10,7 @@ Getting static type information about Ruby files isn't the difficult part, the p
10
10
 
11
11
  Inserting Crystal types into Ruby code is a no-go, because that causes `Lint/Syntax` errors in RuboCop.
12
12
 
13
- Possible paths foward:
13
+ Possible paths forward:
14
14
  - Modify the parser to accept Crystal type declarations, or at least not break on them.
15
15
  - Modify Crystal to accept type declarations from `.rbs` and/or `.rbi` files.
16
16
  - Modify Crystal to accept type declarations from sorbet/rbs-inline/other annotations.
data/config/default.yml CHANGED
@@ -1,10 +1,27 @@
1
1
  AllCops:
2
2
  DisabledByDefault: true
3
+ inherit_mode:
4
+ merge:
5
+ - Include
6
+ Include:
7
+ - '**/*.cr'
8
+
9
+ Crystal/EnumerableReduce:
10
+ Description: 'This cop replaces .inject with .reduce and converts reducer functions to their block counterparts.'
11
+ Enabled: true
12
+
13
+ Crystal/EnumerableSize:
14
+ Description: 'This cop replaces .length and .count with .size when they are used as aliases for .size'
15
+ Enabled: true
3
16
 
4
17
  Crystal/FileExtension:
5
18
  Description: 'This cop renames files ending in `.rb` to `.cr`.'
6
19
  Enabled: true
7
20
 
21
+ Crystal/FileReadLines:
22
+ Description: "This cop replaces Ruby's IO.readlines with Crystal's File.read_lines or IO.each_line."
23
+ Enabled: true
24
+
8
25
  Crystal/InterpolationInSingleQuotes:
9
26
  Description: "This cop uses %q in place of ' if the enclosed string would be affected by interpolation."
10
27
  Enabled: true
@@ -13,6 +30,14 @@ Crystal/MethodNameStartingWithUppercaseLetter:
13
30
  Description: 'This cop detects method names that start with uppercase letters.'
14
31
  Enabled: true
15
32
 
33
+ Crystal/MethodReturningChar:
34
+ Description: 'This cop detects methods that, in Crystal, return the Char type instead of a 1-character string, and modifies them accordingly.'
35
+ Enabled: true
36
+
37
+ Crystal/RequireAtTopLevel:
38
+ Description: 'This cop detects instances of require that are not at the top level.'
39
+ Enabled: true
40
+
16
41
  Crystal/RequireRelative:
17
42
  Description: 'This cop replaces require_relative with require while maintaining behavior.'
18
43
  Enabled: true
@@ -0,0 +1,58 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Crystal
4
+ # Ruby has Enumerable.inject, which can take either a block or a reducer function,
5
+ # and an optional initial value. Enumerable.reduce is an alias of this.
6
+ # Crystal only has Enumerable.reduce, which does not support reducer functions.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # x.inject { |r,v| r + v }
11
+ # x.inject(:+)
12
+ # x.reduce(:+)
13
+ #
14
+ # # good
15
+ # x.reduce { |r,v| r + v }
16
+ #
17
+ # # bad
18
+ # x.inject(y) { |r,v| r + v }
19
+ # x.inject(y, :+)
20
+ # x.reduce(y, :+)
21
+ #
22
+ # # good
23
+ # x.reduce(y) { |r,v| r + v }
24
+ #
25
+ class EnumerableReduce < Base
26
+ extend AutoCorrector
27
+
28
+ MSG = 'Crystal has .reduce instead of .inject'
29
+ RESTRICT_ON_SEND = %i[inject reduce]
30
+
31
+ def on_send(node)
32
+ new_node = node.receiver.source
33
+ new_node << if node.csend_type?
34
+ '&.'
35
+ else
36
+ '.'
37
+ end
38
+ new_node << 'reduce'
39
+ if node.arguments?
40
+ if node.parent&.block_type?
41
+ new_node << "(#{node.first_argument.source})"
42
+ else
43
+ new_node << "(#{node.first_argument.source})" if node.arguments.size == 2
44
+ new_node << " { |r,v| r.#{node.last_argument.source.delete_prefix(':')}(v) }"
45
+ end
46
+ end
47
+
48
+ return if node.source == new_node
49
+
50
+ add_offense(node.selector) do |corrector|
51
+ corrector.replace(node, new_node)
52
+ end
53
+ end
54
+ alias on_csend on_send
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Crystal
4
+ # Crystal does not have the .length or .count methods as aliases to .size
5
+ #
6
+ # @example
7
+ # # bad
8
+ # x.length
9
+ # x.count
10
+ #
11
+ # # good
12
+ # x.size
13
+ #
14
+ class EnumerableSize < Base
15
+ extend AutoCorrector
16
+
17
+ MSG = 'Crystal does not have the .length or .count methods as aliases to .size'
18
+ RESTRICT_ON_SEND = %i[count length]
19
+
20
+ def on_send(node)
21
+ return if node.arguments? || node.parent&.block_type?
22
+
23
+ add_offense(node.selector) do |corrector|
24
+ corrector.replace(node.selector, 'size')
25
+ end
26
+ end
27
+ alias on_csend on_send
28
+ end
29
+ end
30
+ end
31
+ end
@@ -13,6 +13,7 @@ module RuboCop
13
13
  #
14
14
  class FileExtension < Base
15
15
  extend AutoCorrector
16
+
16
17
  MSG = 'Crystal files have `.cr` extensions, while Ruby files have `.rb` extensions.'
17
18
 
18
19
  def on_new_investigation
@@ -0,0 +1,115 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Crystal
4
+ # Ruby has IO.readlines, which supports chomp (false by default), limit and separator arguments, and has special behaviors for empty and nil separators.
5
+ # Crystal has File.read_lines, which only supports the chomp (true by default) argument, and IO.each_line, which supports limit and separator arguments,
6
+ # but does not have special behaviors for empty and nil separators, so we recreate that ourselves.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # File.readlines('foo')
11
+ # IO.readlines('foo')
12
+ #
13
+ # # good
14
+ # File.read_lines('foo', chomp: false)
15
+ #
16
+ # # bad
17
+ # File.readlines('foo', 'b', 3)
18
+ # IO.readlines('foo', 'b', 3)
19
+ #
20
+ # # good
21
+ # File.open('foo').each_line('b', 3).to_a
22
+ #
23
+ # # bad
24
+ # File.readlines('foo', nil)
25
+ # IO.readlines('foo', nil)
26
+ #
27
+ # # bad
28
+ # File.readlines("foo", '')
29
+ # IO.readlines("foo", '')
30
+ #
31
+ # # good
32
+ # File.open("foo").each_line("\n").to_a.join.split(/\n{2,}/).reject { |e| e.empty? }.reverse.map_with_index {|e, i| i == 0 ? e : "#{e}\n\n" }.reverse
33
+ #
34
+ class FileReadLines < Base
35
+ extend AutoCorrector
36
+
37
+ MSG = 'Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line'
38
+ RESTRICT_ON_SEND = %i[readlines]
39
+
40
+ # @!method no_arguments?(node)
41
+ def_node_matcher :no_arguments?, <<~PATTERN
42
+ (send (send (const {nil? cbase} :File) ... ) :readlines)
43
+ PATTERN
44
+
45
+ # @!method path_argument?(node)
46
+ def_node_matcher :path_argument?, <<~PATTERN
47
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_))
48
+ PATTERN
49
+
50
+ # @!method path_and_chomp_argument?(node)
51
+ def_node_matcher :path_and_chomp_argument?, <<~PATTERN
52
+ (send (const {nil? cbase} {:File :IO}) :readlines (str _) (hash (pair (sym :chomp) _)))
53
+ PATTERN
54
+
55
+ # TODO: Do this properly once https://github.com/rubocop/rubocop-ast/pull/386 is merged.
56
+ # @!method path_and_empty_separator_argument?(node)
57
+ def_node_matcher :path_and_empty_separator_argument?, <<~PATTERN
58
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_) (str empty?))
59
+ PATTERN
60
+
61
+ # @!method path_and_nil_separator_argument?(node)
62
+ def_node_matcher :path_and_nil_separator_argument?, <<~PATTERN
63
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_) nil)
64
+ PATTERN
65
+
66
+ # @!method path_and_separator_argument?(node)
67
+ def_node_matcher :path_and_separator_argument?, <<~PATTERN
68
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_) (str $_))
69
+ PATTERN
70
+
71
+ # @!method path_and_limiter_argument?(node)
72
+ def_node_matcher :path_and_limiter_argument?, <<~PATTERN
73
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_) (int $_))
74
+ PATTERN
75
+
76
+ # @!method path_separator_and_limiter_argument?(node)
77
+ def_node_matcher :path_separator_and_limiter_argument?, <<~PATTERN
78
+ (send (const {nil? cbase} {:File :IO}) :readlines (str $_) (str $_) (int $_))
79
+ PATTERN
80
+
81
+ def on_send(node)
82
+ if no_arguments?(node)
83
+ autocorrect(node, node.source.sub('.readlines', '.each_line.to_a'))
84
+ elsif (path = path_argument?(node))
85
+ autocorrect(node, "File.read_lines(\"#{path}\", chomp: false)")
86
+ elsif path_and_chomp_argument?(node)
87
+ autocorrect(node, node.source.sub(/(File|IO)\.readlines/, 'File.read_lines'))
88
+ elsif (path = path_and_empty_separator_argument?(node))
89
+ # TODO: There's probably a slightly cleaner way to replicate Ruby's "paragraph" separator behavior.
90
+ autocorrect(node, "File.open(\"#{path}\").each_line(\"\\n\").to_a.join.split(/\\n{2,}/).reject { |e| e.empty? }.reverse.map_with_index {|e, i| i == 0 ? e : \"\#{e}\\n\\n\" }.reverse")
91
+ elsif (path = path_and_nil_separator_argument?(node))
92
+ # TODO: Crystal might technically support this more cleanly, although this doesn't appear to be documented.
93
+ # https://github.com/crystal-lang/crystal/blob/1.17.1/src/io.cr#L818-L822
94
+ autocorrect(node, "[File.open(\"#{path}\").each_line(chomp: false).to_a.join]")
95
+ elsif (path, separator = path_and_separator_argument?(node))
96
+ autocorrect(node, "File.open(\"#{path}\").each_line(\"#{separator}\").to_a")
97
+ elsif (path, limiter = path_and_limiter_argument?(node))
98
+ autocorrect(node, "File.open(\"#{path}\").each_line(#{limiter}).to_a")
99
+ elsif (path, separator, limiter = path_separator_and_limiter_argument?(node))
100
+ autocorrect(node, "File.open(\"#{path}\").each_line(\"#{separator}\", #{limiter}).to_a")
101
+ end
102
+ end
103
+ alias on_csend on_send
104
+
105
+ private
106
+
107
+ def autocorrect(node, replacement)
108
+ add_offense(node.selector) do |corrector|
109
+ corrector.replace(node, replacement)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -16,6 +16,7 @@ module RuboCop
16
16
  #
17
17
  class InterpolationInSingleQuotes < Base
18
18
  extend AutoCorrector
19
+
19
20
  MSG = 'Crystal does not support the use of single-quote deliminated strings to avoid interpolation.'
20
21
 
21
22
  def on_str(node)
@@ -19,7 +19,7 @@ module RuboCop
19
19
  # foo(bar)
20
20
  #
21
21
  class MethodNameStartingWithUppercaseLetter < Base
22
- MSG = 'Method names must start with a lowecase letter in Crystal.'
22
+ MSG = 'Method names must start with a lowercase letter in Crystal.'
23
23
 
24
24
  def on_def(node)
25
25
  add_offense(node.loc.name) if node.method_name.to_s.chr.capitalize!.nil?
@@ -0,0 +1,54 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Crystal
4
+ # In Crystal, certain methods return a single character of type Char, while in Ruby they return a 1-character string.
5
+ # Chars and 1-character strings are treated differently in Crystal, and will not count as equal even if they contain the same content.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # x.chars
10
+ #
11
+ # # good
12
+ # x.chars.map { |c| c.to_s }
13
+ #
14
+ # # bad
15
+ # x.each_char { |c| y << c }
16
+ #
17
+ # # good
18
+ # x.each_char { |c| y << c.to_s }
19
+ #
20
+ class MethodReturningChar < Base
21
+ extend AutoCorrector
22
+
23
+ MSG = 'In Crystal, this method returns the Char type instead of a 1-character string.'
24
+ RESTRICT_ON_SEND = %i[chars each_char]
25
+
26
+ # @!method map_to_s?(node)
27
+ def_node_matcher :map_to_s?, <<~PATTERN
28
+ (block (send (call (...) :chars) :map) (args (arg :c)) (send (lvar :c) :to_s))
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ if node.method?(:chars) && !map_to_s?(node.parent&.parent)
33
+ add_offense(node.selector) do |corrector|
34
+ corrector.insert_after(node.selector, '.map { |c| c.to_s }')
35
+ end
36
+ elsif node.method?(:each_char)
37
+ nodes_to_correct = []
38
+ node.parent.body.each_descendant do |n|
39
+ # If a node is an lvar with the same name as the character argument and it does not have a .to_s, it needs to be corrected.
40
+ nodes_to_correct << n if n.lvar_type? && n.source == node.parent.first_argument.source.gsub('|', '') && !n.parent.method?(:to_s)
41
+ end
42
+
43
+ return if nodes_to_correct.empty?
44
+
45
+ add_offense(node.selector) do |corrector|
46
+ nodes_to_correct.each { |n| corrector.insert_after(n, '.to_s') }
47
+ end
48
+ end
49
+ end
50
+ alias on_csend on_send
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Crystal
4
+ # As of Crystal 0.7.7, require is only allowed at the top level:
5
+ # https://github.com/crystal-lang/crystal/releases/tag/0.7.7
6
+ # ^ "(breaking change) require is now only allowed at the top-level, never inside other types or methods."
7
+ #
8
+ # @example
9
+ # # bad
10
+ # def foo
11
+ # require 'bar'
12
+ # end
13
+ #
14
+ # class Foo
15
+ # require 'bar'
16
+ # end
17
+ #
18
+ # module Foo
19
+ # require 'bar'
20
+ # end
21
+ #
22
+ # # good
23
+ # require 'bar'
24
+ #
25
+ class RequireAtTopLevel < Base
26
+ MSG = 'Crystal does not allow require anywhere other than the top level.'
27
+ RESTRICT_ON_SEND = [:require]
28
+
29
+ def on_send(node)
30
+ add_offense(node) if %i[def class module].include?(node.parent&.type)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -19,6 +19,7 @@ module RuboCop
19
19
  #
20
20
  class RequireRelative < Base
21
21
  extend AutoCorrector
22
+
22
23
  MSG = 'Crystal does not support require_relative.'
23
24
  RESTRICT_ON_SEND = [:require_relative]
24
25
 
@@ -1,4 +1,9 @@
1
+ require_relative 'crystal/enumerable_reduce'
2
+ require_relative 'crystal/enumerable_size'
1
3
  require_relative 'crystal/file_extension'
4
+ require_relative 'crystal/file_read_lines'
2
5
  require_relative 'crystal/interpolation_in_single_quotes'
3
6
  require_relative 'crystal/method_name_starting_with_uppercase_letter'
7
+ require_relative 'crystal/method_returning_char'
8
+ require_relative 'crystal/require_at_top_level'
4
9
  require_relative 'crystal/require_relative'
@@ -6,7 +6,7 @@ module RuboCop
6
6
  def about
7
7
  LintRoller::About.new(
8
8
  name: 'rubocop-crystal',
9
- version: '0.0.3',
9
+ version: '0.0.4',
10
10
  homepage: 'https://github.com/Zopolis4/rubocop-crystal',
11
11
  description: 'A RuboCop extension for converting Ruby to Crystal.'
12
12
  )
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = 'rubocop-crystal'
3
3
  spec.summary = 'A RuboCop extension for converting Ruby to Crystal.'
4
- spec.version = '0.0.3'
4
+ spec.version = '0.0.4'
5
5
  spec.license = 'GPL-3.0-or-later'
6
6
  spec.author = 'Zopolis4'
7
7
  spec.email = 'creatorsmithmdt@gmail.com'
@@ -13,5 +13,5 @@ Gem::Specification.new do |spec|
13
13
  spec.require_paths = ['lib']
14
14
 
15
15
  spec.add_dependency 'lint_roller'
16
- spec.add_dependency 'rubocop', '>= 1.72.1'
16
+ spec.add_dependency 'rubocop', '>= 1.80.2'
17
17
  end
@@ -0,0 +1,101 @@
1
+ RSpec.describe RuboCop::Cop::Crystal::EnumerableReduce, :config do
2
+ it 'registers an offense when .inject is used with a block and no initial value' do
3
+ expect_offense(<<~RUBY)
4
+ x.inject { |r,v| r + v }
5
+ ^^^^^^ Crystal has .reduce instead of .inject
6
+ RUBY
7
+
8
+ expect_correction(<<~RUBY)
9
+ x.reduce { |r,v| r + v }
10
+ RUBY
11
+ end
12
+
13
+ it 'registers an offense when &.inject is used with a block and no initial value' do
14
+ expect_offense(<<~RUBY)
15
+ x&.inject { |r,v| r + v }
16
+ ^^^^^^ Crystal has .reduce instead of .inject
17
+ RUBY
18
+
19
+ expect_correction(<<~RUBY)
20
+ x&.reduce { |r,v| r + v }
21
+ RUBY
22
+ end
23
+
24
+ it 'registers an offense when .inject is used with a block and an initial value' do
25
+ expect_offense(<<~RUBY)
26
+ x.inject(y) { |r,v| r + v }
27
+ ^^^^^^ Crystal has .reduce instead of .inject
28
+ RUBY
29
+
30
+ expect_correction(<<~RUBY)
31
+ x.reduce(y) { |r,v| r + v }
32
+ RUBY
33
+ end
34
+
35
+ it 'registers an offense when .inject is used with a reducer function and no initial value' do
36
+ expect_offense(<<~RUBY)
37
+ x.inject(:+)
38
+ ^^^^^^ Crystal has .reduce instead of .inject
39
+ RUBY
40
+
41
+ expect_correction(<<~RUBY)
42
+ x.reduce { |r,v| r.+(v) }
43
+ RUBY
44
+ end
45
+
46
+ it 'registers an offense when .inject is used with a reducer function and an initial value' do
47
+ expect_offense(<<~RUBY)
48
+ x.inject(4, :+)
49
+ ^^^^^^ Crystal has .reduce instead of .inject
50
+ RUBY
51
+
52
+ expect_correction(<<~RUBY)
53
+ x.reduce(4) { |r,v| r.+(v) }
54
+ RUBY
55
+ end
56
+
57
+ it 'registers an offense when .reduce is used with a reducer function and no initial value' do
58
+ expect_offense(<<~RUBY)
59
+ x.reduce(:prepend)
60
+ ^^^^^^ Crystal has .reduce instead of .inject
61
+ RUBY
62
+
63
+ expect_correction(<<~RUBY)
64
+ x.reduce { |r,v| r.prepend(v) }
65
+ RUBY
66
+ end
67
+
68
+ it 'registers an offense when &.reduce is used with a reducer function and no initial value' do
69
+ expect_offense(<<~RUBY)
70
+ x&.reduce(:prepend)
71
+ ^^^^^^ Crystal has .reduce instead of .inject
72
+ RUBY
73
+
74
+ expect_correction(<<~RUBY)
75
+ x&.reduce { |r,v| r.prepend(v) }
76
+ RUBY
77
+ end
78
+
79
+ it 'registers an offense when .reduce is used with a reducer function and an initial value' do
80
+ expect_offense(<<~RUBY)
81
+ x.reduce("foo", :prepend)
82
+ ^^^^^^ Crystal has .reduce instead of .inject
83
+ RUBY
84
+
85
+ expect_correction(<<~RUBY)
86
+ x.reduce("foo") { |r,v| r.prepend(v) }
87
+ RUBY
88
+ end
89
+
90
+ it 'does not register an offense when .reduce is used with a block and no initial value' do
91
+ expect_no_offenses(<<~RUBY)
92
+ x.reduce { |r,v| r + v }
93
+ RUBY
94
+ end
95
+
96
+ it 'does not register an offense when &.reduce is used with a block and no initial value' do
97
+ expect_no_offenses(<<~RUBY)
98
+ x&.reduce { |r,v| r + v }
99
+ RUBY
100
+ end
101
+ end
@@ -0,0 +1,64 @@
1
+ RSpec.describe RuboCop::Cop::Crystal::EnumerableSize, :config do
2
+ it 'registers an offense when .length is used' do
3
+ expect_offense(<<~RUBY)
4
+ x.length
5
+ ^^^^^^ Crystal does not have the .length or .count methods as aliases to .size
6
+ RUBY
7
+
8
+ expect_correction(<<~RUBY)
9
+ x.size
10
+ RUBY
11
+ end
12
+
13
+ it 'registers an offense when &.length is used' do
14
+ expect_offense(<<~RUBY)
15
+ x&.length
16
+ ^^^^^^ Crystal does not have the .length or .count methods as aliases to .size
17
+ RUBY
18
+
19
+ expect_correction(<<~RUBY)
20
+ x&.size
21
+ RUBY
22
+ end
23
+
24
+ it 'registers an offense when .count is used with no argument and no block' do
25
+ expect_offense(<<~RUBY)
26
+ x.count
27
+ ^^^^^ Crystal does not have the .length or .count methods as aliases to .size
28
+ RUBY
29
+
30
+ expect_correction(<<~RUBY)
31
+ x.size
32
+ RUBY
33
+ end
34
+
35
+ it 'does not register an offense when .count is used with an argument and no block' do
36
+ expect_no_offenses(<<~RUBY)
37
+ x.count(y)
38
+ RUBY
39
+ end
40
+
41
+ it 'does not register an offense when &.count is used with an argument and no block' do
42
+ expect_no_offenses(<<~RUBY)
43
+ x&.count(y)
44
+ RUBY
45
+ end
46
+
47
+ it 'does not register an offense when .count is used with a block and no argument' do
48
+ expect_no_offenses(<<~RUBY)
49
+ x.count {|e| e > 2 }
50
+ RUBY
51
+ end
52
+
53
+ it 'does not register an offense when .count is used with an argument and a block' do
54
+ expect_no_offenses(<<~RUBY)
55
+ x.count(y) {|e| e > 2 }
56
+ RUBY
57
+ end
58
+
59
+ it 'does not register an offense when .size is used' do
60
+ expect_no_offenses(<<~RUBY)
61
+ x.size
62
+ RUBY
63
+ end
64
+ end
@@ -0,0 +1,156 @@
1
+ RSpec.describe RuboCop::Cop::Crystal::FileReadLines, :config do
2
+ it 'registers an offense when File.readlines is used with a path argument' do
3
+ expect_offense(<<~RUBY)
4
+ File.readlines("foo")
5
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
6
+ RUBY
7
+
8
+ expect_correction(<<~RUBY)
9
+ File.read_lines("foo", chomp: false)
10
+ RUBY
11
+ end
12
+
13
+ it 'registers an offense when IO.readlines is used with a path argument' do
14
+ expect_offense(<<~RUBY)
15
+ IO.readlines("foo")
16
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
17
+ RUBY
18
+
19
+ expect_correction(<<~RUBY)
20
+ File.read_lines("foo", chomp: false)
21
+ RUBY
22
+ end
23
+
24
+ it 'registers an offense when File.readlines is used with a path argument and chomp: false' do
25
+ expect_offense(<<~RUBY)
26
+ File.readlines("foo", chomp: false)
27
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
28
+ RUBY
29
+
30
+ expect_correction(<<~RUBY)
31
+ File.read_lines("foo", chomp: false)
32
+ RUBY
33
+ end
34
+
35
+ it 'registers an offense when File.readlines is used with a path argument and chomp: true' do
36
+ expect_offense(<<~RUBY)
37
+ File.readlines("foo", chomp: true)
38
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
39
+ RUBY
40
+
41
+ expect_correction(<<~RUBY)
42
+ File.read_lines("foo", chomp: true)
43
+ RUBY
44
+ end
45
+
46
+ it 'registers an offense when File.readlines is used with a path and a single-character separator argument' do
47
+ expect_offense(<<~RUBY)
48
+ File.readlines("foo", "a")
49
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
50
+ RUBY
51
+
52
+ expect_correction(<<~RUBY)
53
+ File.open("foo").each_line("a").to_a
54
+ RUBY
55
+ end
56
+
57
+ it 'registers an offense when File.readlines is used with a path and a multi-character separator argument' do
58
+ expect_offense(<<~RUBY)
59
+ File.readlines("foo", "quz")
60
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
61
+ RUBY
62
+
63
+ expect_correction(<<~RUBY)
64
+ File.open("foo").each_line("quz").to_a
65
+ RUBY
66
+ end
67
+
68
+ it 'registers an offense when File.readlines is used with a path and a nil separator argument' do
69
+ expect_offense(<<~RUBY)
70
+ File.readlines("foo", nil)
71
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
72
+ RUBY
73
+
74
+ expect_correction(<<~RUBY)
75
+ [File.open("foo").each_line(chomp: false).to_a.join]
76
+ RUBY
77
+ end
78
+
79
+ it 'registers an offense when File.readlines is used with a path and an empty string separator argument' do
80
+ expect_offense(<<~RUBY)
81
+ File.readlines("foo", "")
82
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
83
+ RUBY
84
+
85
+ expect_correction(<<~'RUBY')
86
+ File.open("foo").each_line("\n").to_a.join.split(/\n{2,}/).reject { |e| e.empty? }.reverse.map_with_index {|e, i| i == 0 ? e : "#{e}\n\n" }.reverse
87
+ RUBY
88
+ end
89
+
90
+ it 'registers an offense when File.readlines is used with a path and a limiter argument' do
91
+ expect_offense(<<~RUBY)
92
+ File.readlines("foo", 2)
93
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
94
+ RUBY
95
+
96
+ expect_correction(<<~RUBY)
97
+ File.open("foo").each_line(2).to_a
98
+ RUBY
99
+ end
100
+
101
+ it 'registers an offense when File.readlines is used with a path, single-character separator, and limiter argument' do
102
+ expect_offense(<<~RUBY)
103
+ File.readlines("foo", "b", 3)
104
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
105
+ RUBY
106
+
107
+ expect_correction(<<~RUBY)
108
+ File.open("foo").each_line("b", 3).to_a
109
+ RUBY
110
+ end
111
+
112
+ it 'registers an offense when File.readlines is used with a path, multi-character separator, and limiter argument' do
113
+ expect_offense(<<~RUBY)
114
+ File.readlines("foo", "quz", 3)
115
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
116
+ RUBY
117
+
118
+ expect_correction(<<~RUBY)
119
+ File.open("foo").each_line("quz", 3).to_a
120
+ RUBY
121
+ end
122
+
123
+ it 'registers an offense when .readlines is used on a File object' do
124
+ expect_offense(<<~RUBY)
125
+ File.open("foo").readlines
126
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
127
+ RUBY
128
+
129
+ expect_correction(<<~RUBY)
130
+ File.open("foo").each_line.to_a
131
+ RUBY
132
+ end
133
+
134
+ it 'registers an offense when .readlines is used on a chained File object' do
135
+ expect_offense(<<~RUBY)
136
+ File.new(File.expand_path("foo", "bar")).readlines
137
+ ^^^^^^^^^ Ruby has IO.readlines, while Crystal has File.read_lines and IO.each_line
138
+ RUBY
139
+
140
+ expect_correction(<<~RUBY)
141
+ File.new(File.expand_path("foo", "bar")).each_line.to_a
142
+ RUBY
143
+ end
144
+
145
+ it 'does not register an offense when .readlines is used from a non File/IO class' do
146
+ expect_no_offenses(<<~RUBY)
147
+ Bar.readlines("foo")
148
+ RUBY
149
+ end
150
+
151
+ it 'does not register an offense when .readlines is used on a non File/IO object' do
152
+ expect_no_offenses(<<~RUBY)
153
+ Bar.open("foo").readlines
154
+ RUBY
155
+ end
156
+ end
@@ -2,7 +2,7 @@ RSpec.describe RuboCop::Cop::Crystal::MethodNameStartingWithUppercaseLetter, :co
2
2
  it 'registers an offense when the first letter of a method name is capitalized' do
3
3
  expect_offense(<<~RUBY)
4
4
  def Foo
5
- ^^^ Method names must start with a lowecase letter in Crystal.
5
+ ^^^ Method names must start with a lowercase letter in Crystal.
6
6
  bar
7
7
  end
8
8
  RUBY
@@ -11,7 +11,7 @@ RSpec.describe RuboCop::Cop::Crystal::MethodNameStartingWithUppercaseLetter, :co
11
11
  it 'registers an offense when all the letters of a method name are capitalized' do
12
12
  expect_offense(<<~RUBY)
13
13
  def FOO
14
- ^^^ Method names must start with a lowecase letter in Crystal.
14
+ ^^^ Method names must start with a lowercase letter in Crystal.
15
15
  bar
16
16
  end
17
17
  RUBY
@@ -0,0 +1,150 @@
1
+ RSpec.describe RuboCop::Cop::Crystal::MethodReturningChar, :config do
2
+ it 'registers an offense when .chars is used without converting the elements of the array to strings' do
3
+ expect_offense(<<~RUBY)
4
+ x.chars
5
+ ^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
6
+ RUBY
7
+
8
+ expect_correction(<<~RUBY)
9
+ x.chars.map { |c| c.to_s }
10
+ RUBY
11
+ end
12
+
13
+ it 'registers an offense when &.chars is used without converting the elements of the array to strings' do
14
+ expect_offense(<<~RUBY)
15
+ x&.chars
16
+ ^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
17
+ RUBY
18
+
19
+ expect_correction(<<~RUBY)
20
+ x&.chars.map { |c| c.to_s }
21
+ RUBY
22
+ end
23
+
24
+ it 'registers an offense when .chars is used in a chain ending in a block without converting the elements of the array to strings' do
25
+ expect_offense(<<~RUBY)
26
+ x.chars.each { |c| puts c}
27
+ ^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
28
+ RUBY
29
+
30
+ expect_correction(<<~RUBY)
31
+ x.chars.map { |c| c.to_s }.each { |c| puts c}
32
+ RUBY
33
+ end
34
+
35
+ it 'registers an offense when .chars is used on a string literal in a chain ending in a block without converting the elements of the array to strings' do
36
+ expect_offense(<<~RUBY)
37
+ "foo".chars.each { |c| puts c}
38
+ ^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
39
+ RUBY
40
+
41
+ expect_correction(<<~RUBY)
42
+ "foo".chars.map { |c| c.to_s }.each { |c| puts c}
43
+ RUBY
44
+ end
45
+
46
+ it 'registers an offense when .chars is used on a variable in a chain ending in a block without converting the elements of the array to strings' do
47
+ expect_offense(<<~RUBY)
48
+ x = "foo"
49
+ x.chars.each { |c| puts c}
50
+ ^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
51
+ RUBY
52
+
53
+ expect_correction(<<~RUBY)
54
+ x = "foo"
55
+ x.chars.map { |c| c.to_s }.each { |c| puts c}
56
+ RUBY
57
+ end
58
+
59
+ it 'registers an offense when .each_char is used without converting the character to a string' do
60
+ expect_offense(<<~RUBY)
61
+ x.each_char { |c| puts c }
62
+ ^^^^^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
63
+ RUBY
64
+
65
+ expect_correction(<<~RUBY)
66
+ x.each_char { |c| puts c.to_s }
67
+ RUBY
68
+ end
69
+
70
+ it 'registers an offense when .each_char with a multiline block is used without converting each instance of the character to a string' do
71
+ expect_offense(<<~RUBY)
72
+ x.each_char do |c|
73
+ ^^^^^^^^^ In Crystal, this method returns the Char type instead of a 1-character string.
74
+ puts foo if c == y
75
+ bar
76
+ z << c
77
+ end
78
+ RUBY
79
+
80
+ expect_correction(<<~RUBY)
81
+ x.each_char do |c|
82
+ puts foo if c.to_s == y
83
+ bar
84
+ z << c.to_s
85
+ end
86
+ RUBY
87
+ end
88
+
89
+ it 'does not register an an offense when .chars is used with converting the elements of the array to strings' do
90
+ expect_no_offenses(<<~RUBY)
91
+ x.chars.map { |c| c.to_s }
92
+ RUBY
93
+ end
94
+
95
+ it 'does not register an an offense when &.chars is used with converting the elements of the array to strings' do
96
+ expect_no_offenses(<<~RUBY)
97
+ x&.chars.map { |c| c.to_s }
98
+ RUBY
99
+ end
100
+
101
+ it 'does not register an offense when .chars is used in a chain ending in a block with converting the elements of the array to strings' do
102
+ expect_no_offenses(<<~RUBY)
103
+ x.chars.map { |c| c.to_s }.each { |c| puts c}
104
+ RUBY
105
+ end
106
+
107
+ it 'does not register an offense when .chars is used on a string literal in a chain ending in a block with converting the elements of the array to strings' do
108
+ expect_no_offenses(<<~RUBY)
109
+ "foo".chars.map { |c| c.to_s }.each { |c| puts c}
110
+ RUBY
111
+ end
112
+
113
+ it 'does not register an offense when .chars is used on a variable in a chain ending in a block with converting the elements of the array to strings' do
114
+ expect_no_offenses(<<~RUBY)
115
+ x = "foo"
116
+ x.chars.map { |c| c.to_s }.each { |c| puts c}
117
+ RUBY
118
+ end
119
+
120
+ it 'does not register an offense when .each_char is used with converting the character to a string' do
121
+ expect_no_offenses(<<~RUBY)
122
+ x.each_char { |c| puts c.to_s }
123
+ RUBY
124
+ end
125
+
126
+ it 'does not register an offense when .each_char with a multiline block is used with converting each instance of the character to a string' do
127
+ expect_no_offenses(<<~RUBY)
128
+ x.each_char do |c|
129
+ puts foo if c.to_s == y
130
+ bar
131
+ z << c.to_s
132
+ end
133
+ RUBY
134
+ end
135
+
136
+ it 'does not register an offense when .each_char is used with no instances of the character' do
137
+ expect_no_offenses(<<~RUBY)
138
+ x.each_char { |c| puts x }
139
+ RUBY
140
+ end
141
+
142
+ it 'does not register an offense when .each_char with a multiline block is used with no instances of the character' do
143
+ expect_no_offenses(<<~RUBY)
144
+ x.each_char do |c|
145
+ puts x
146
+ foo if y == z
147
+ end
148
+ RUBY
149
+ end
150
+ end
@@ -0,0 +1,42 @@
1
+ RSpec.describe RuboCop::Cop::Crystal::RequireAtTopLevel, :config do
2
+ it 'registers an offense when require is inside a function' do
3
+ expect_offense(<<~RUBY)
4
+ def foo
5
+ require 'bar'
6
+ ^^^^^^^^^^^^^ Crystal does not allow require anywhere other than the top level.
7
+ end
8
+ RUBY
9
+ end
10
+
11
+ it 'registers an offense when require is inside a class' do
12
+ expect_offense(<<~RUBY)
13
+ class Foo
14
+ require 'bar'
15
+ ^^^^^^^^^^^^^ Crystal does not allow require anywhere other than the top level.
16
+ end
17
+ RUBY
18
+ end
19
+
20
+ it 'registers an offense when require is inside a module' do
21
+ expect_offense(<<~RUBY)
22
+ module Foo
23
+ require 'bar'
24
+ ^^^^^^^^^^^^^ Crystal does not allow require anywhere other than the top level.
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ it 'does not register an offense when require is at the top level' do
30
+ expect_no_offenses(<<~RUBY)
31
+ require 'bar'
32
+ RUBY
33
+ end
34
+
35
+ it 'does not register an offense when a funciton does not contain require' do
36
+ expect_no_offenses(<<~RUBY)
37
+ def foo
38
+ bar
39
+ end
40
+ RUBY
41
+ end
42
+ end
data/test/string.rb CHANGED
@@ -3,7 +3,7 @@ require_relative 'harness'
3
3
  =begin
4
4
  The Alternative Instruction Set is a relatively unknown 32-bit RISC ISA.
5
5
  It is found inside certain VIA C3 CPUs, and is responsible for emulating x86 instructions.
6
- This isn't relevant in the slightest, but I had to put something in this commment, and I think it's cool.
6
+ This isn't relevant in the slightest, but I had to put something in this comment, and I think it's cool.
7
7
  =end
8
8
 
9
9
  Test.assert_equal 'foo', "foo"
@@ -25,6 +25,9 @@ Test.assert_unequal '#{1 + 1}', '2'
25
25
 
26
26
  Test.assert_equal '#{foo}'"bar", '#{foo}bar'
27
27
 
28
+ Test.assert_equal 'a'.chars[0], "a"
29
+ "bbb".each_char {|c| Test.assert_equal c, 'b' }
30
+
28
31
  i = 3
29
32
 
30
33
  while i > 0 do
@@ -36,3 +39,37 @@ until i > 3 do
36
39
  i += 1
37
40
  end
38
41
  Test.assert_equal 4, i
42
+
43
+ x = [0, 1, 1, 2, 3, 4]
44
+
45
+ Test.assert_equal x.length, 6
46
+ Test.assert_equal x.count, 6
47
+ Test.assert_equal x.size, 6
48
+
49
+ Test.assert_equal x.count(1), 2
50
+ Test.assert_equal x.count {|e| e > 1}, 3
51
+
52
+ Test.assert_equal x.inject { |r,v| r + v }, 11
53
+ Test.assert_equal x.inject(4) { |r,v| r - v }, -7
54
+
55
+ Test.assert_equal x.inject(:+), 11
56
+ Test.assert_equal x.reduce(:+), 11
57
+ Test.assert_equal x.inject(4, :-), -7
58
+ Test.assert_equal x.reduce(4, :-), -7
59
+
60
+ # TODO: This could be done using a true Tempfile, but Crystal removed theirs in https://github.com/crystal-lang/crystal/pull/6485.
61
+ File.write('temp', "first line\nsecond line\n\nfourth line\nfifth line")
62
+
63
+ Test.assert_equal File.readlines('temp'), ["first line\n", "second line\n", "\n", "fourth line\n", 'fifth line']
64
+ Test.assert_equal File.readlines('temp', chomp: true), ['first line', 'second line', '', 'fourth line', 'fifth line']
65
+ Test.assert_equal File.readlines('temp', ''), ["first line\nsecond line\n\n", "fourth line\nfifth line"]
66
+ Test.assert_equal File.readlines('temp', nil), ["first line\nsecond line\n\nfourth line\nfifth line"]
67
+ Test.assert_equal File.readlines('temp', 't'), ['first', " line\nsecond line\n\nfourt", "h line\nfift", 'h line']
68
+ Test.assert_equal File.readlines('temp', 'fi'), ['fi', "rst line\nsecond line\n\nfourth line\nfi", 'fth line']
69
+ Test.assert_equal File.readlines('temp', 7), ['first l', "ine\n", 'second ', "line\n", "\n", 'fourth ', "line\n", 'fifth l', 'ine']
70
+ # TODO: These are waiting on https://github.com/crystal-lang/crystal/issues/16134.
71
+ # Technically, we could fix the single-character separator by converting it to a Char, but that obviously won't work for the multi-character one, so we need to wait regardless.
72
+ # Test.assert_equal File.readlines('temp', 'e', 10), ['first line', "\nse", 'cond line', "\n\nfourth l", 'ine', "\nfifth lin", 'e']
73
+ # Test.assert_equal File.readlines('temp', 'li', 4), ["firs", "t li", "ne\ns", "econ", "d li", "ne\n\n", "four", "th l", "ine\n", "fift", 'h li', 'ne']
74
+
75
+ File.delete('temp')
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-crystal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zopolis4
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: lint_roller
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 1.72.1
32
+ version: 1.80.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 1.72.1
39
+ version: 1.80.2
40
40
  email: creatorsmithmdt@gmail.com
41
41
  executables: []
42
42
  extensions: []
@@ -53,16 +53,26 @@ files:
53
53
  - README.md
54
54
  - config/default.yml
55
55
  - lib/rubocop-crystal.rb
56
+ - lib/rubocop/cop/crystal/enumerable_reduce.rb
57
+ - lib/rubocop/cop/crystal/enumerable_size.rb
56
58
  - lib/rubocop/cop/crystal/file_extension.rb
59
+ - lib/rubocop/cop/crystal/file_read_lines.rb
57
60
  - lib/rubocop/cop/crystal/interpolation_in_single_quotes.rb
58
61
  - lib/rubocop/cop/crystal/method_name_starting_with_uppercase_letter.rb
62
+ - lib/rubocop/cop/crystal/method_returning_char.rb
63
+ - lib/rubocop/cop/crystal/require_at_top_level.rb
59
64
  - lib/rubocop/cop/crystal/require_relative.rb
60
65
  - lib/rubocop/cop/crystal_cops.rb
61
66
  - lib/rubocop/crystal.rb
62
67
  - lib/rubocop/crystal/plugin.rb
63
68
  - rubocop-crystal.gemspec
69
+ - spec/rubocop/cop/crystal/enumerable_reduce_spec.rb
70
+ - spec/rubocop/cop/crystal/enumerable_size_spec.rb
71
+ - spec/rubocop/cop/crystal/file_read_lines_spec.rb
64
72
  - spec/rubocop/cop/crystal/interpolation_in_single_quotes_spec.rb
65
73
  - spec/rubocop/cop/crystal/method_name_starting_with_uppercase_letter_spec.rb
74
+ - spec/rubocop/cop/crystal/method_returning_char_spec.rb
75
+ - spec/rubocop/cop/crystal/require_at_top_level_spec.rb
66
76
  - spec/rubocop/cop/crystal/require_relative_spec.rb
67
77
  - spec/spec_helper.rb
68
78
  - test/harness.rb
@@ -86,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
96
  - !ruby/object:Gem::Version
87
97
  version: '0'
88
98
  requirements: []
89
- rubygems_version: 3.6.5
99
+ rubygems_version: 3.7.1
90
100
  specification_version: 4
91
101
  summary: A RuboCop extension for converting Ruby to Crystal.
92
102
  test_files: []