rubocop-crystal 0.0.2 → 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 +4 -4
- data/.github/workflows/rspec.yml +16 -0
- data/.github/workflows/rubocop.yml +16 -0
- data/.github/workflows/test.yml +1 -1
- data/.rspec +1 -0
- data/.rubocop.yml +37 -0
- data/CHANGELOG.md +26 -0
- data/README.md +6 -2
- data/config/default.yml +36 -1
- data/lib/rubocop/cop/crystal/enumerable_reduce.rb +58 -0
- data/lib/rubocop/cop/crystal/enumerable_size.rb +31 -0
- data/lib/rubocop/cop/crystal/file_extension.rb +1 -0
- data/lib/rubocop/cop/crystal/file_read_lines.rb +115 -0
- data/lib/rubocop/cop/crystal/interpolation_in_single_quotes.rb +3 -2
- data/lib/rubocop/cop/crystal/method_name_starting_with_uppercase_letter.rb +30 -0
- data/lib/rubocop/cop/crystal/method_returning_char.rb +54 -0
- data/lib/rubocop/cop/crystal/require_at_top_level.rb +35 -0
- data/lib/rubocop/cop/crystal/require_relative.rb +6 -5
- data/lib/rubocop/cop/crystal_cops.rb +6 -0
- data/lib/rubocop/crystal/plugin.rb +28 -0
- data/lib/rubocop-crystal.rb +1 -3
- data/rubocop-crystal.gemspec +5 -2
- data/spec/rubocop/cop/crystal/enumerable_reduce_spec.rb +101 -0
- data/spec/rubocop/cop/crystal/enumerable_size_spec.rb +64 -0
- data/spec/rubocop/cop/crystal/file_read_lines_spec.rb +156 -0
- data/spec/rubocop/cop/crystal/interpolation_in_single_quotes_spec.rb +30 -0
- data/spec/rubocop/cop/crystal/method_name_starting_with_uppercase_letter_spec.rb +35 -0
- data/spec/rubocop/cop/crystal/method_returning_char_spec.rb +150 -0
- data/spec/rubocop/cop/crystal/require_at_top_level_spec.rb +42 -0
- data/spec/rubocop/cop/crystal/require_relative_spec.rb +51 -0
- data/spec/spec_helper.rb +12 -0
- data/test/string.rb +55 -0
- metadata +41 -11
- data/lib/rubocop/crystal/inject.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e7cede16a18df18a0021747a7a5305d1503af8229d0a2790242b3b4ac5e27c2
|
4
|
+
data.tar.gz: 2373953ed41151d8a00749f28db4ac84ff144db91e02413b50e71f4d97c5627b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e33199a6a2a74eb3e7739f0b2355b58e65b89d6bcc9001c4c62e246eea89fa7cb098dfb274da12b7dac220f90de5d672d94f2abb27d078c8a2ec273f8b81f990
|
7
|
+
data.tar.gz: bccb8bb8fba84d6b2208cb578bcbcc7e7a3c6c2202cee707e8fe511018ff128cb4dd6113aefd11912df78a9971183ea8497fe344b684002aa3144279756732c4
|
@@ -0,0 +1,16 @@
|
|
1
|
+
name: RSpec
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
runs-on: ubuntu-latest
|
6
|
+
steps:
|
7
|
+
- name: Download source
|
8
|
+
uses: actions/checkout@v4
|
9
|
+
- name: Install Ruby
|
10
|
+
uses: ruby/setup-ruby@v1
|
11
|
+
with:
|
12
|
+
ruby-version: '3.1.2'
|
13
|
+
- name: Install dependencies
|
14
|
+
run: gem install rspec rubocop
|
15
|
+
- name: Run spec
|
16
|
+
run: rspec
|
@@ -0,0 +1,16 @@
|
|
1
|
+
name: Rubocop
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
runs-on: ubuntu-latest
|
6
|
+
steps:
|
7
|
+
- name: Download source
|
8
|
+
uses: actions/checkout@v4
|
9
|
+
- name: Install Ruby
|
10
|
+
uses: ruby/setup-ruby@v1
|
11
|
+
with:
|
12
|
+
ruby-version: '3.1.2'
|
13
|
+
- name: Install rubocop
|
14
|
+
run: gem install rubocop
|
15
|
+
- name: Run rubocop
|
16
|
+
run: rubocop
|
data/.github/workflows/test.yml
CHANGED
@@ -21,6 +21,6 @@ jobs:
|
|
21
21
|
gem build rubocop-crystal
|
22
22
|
gem install rubocop-crystal
|
23
23
|
- name: Convert test files
|
24
|
-
run: rubocop --
|
24
|
+
run: rubocop --plugin rubocop-crystal -c config/default.yml --fail-level fatal -A test
|
25
25
|
- name: Run tests (Crystal)
|
26
26
|
run: crystal run test/string.cr
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
plugins:
|
2
|
+
- rubocop-internal_affairs
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
NewCops: enable
|
6
|
+
Exclude:
|
7
|
+
- 'test/*'
|
8
|
+
|
9
|
+
Gemspec/RequireMFA:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
# TODO: Find out what ruby version we actually require and then remove this.
|
13
|
+
Gemspec/RequiredRubyVersion:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
InternalAffairs/OnSendWithoutOnCSend:
|
17
|
+
Exclude:
|
18
|
+
- 'lib/rubocop/cop/crystal/require_at_top_level.rb'
|
19
|
+
- 'lib/rubocop/cop/crystal/require_relative.rb'
|
20
|
+
|
21
|
+
Layout/LineLength:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Naming/FileName:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Style/Documentation:
|
31
|
+
Enabled: False
|
32
|
+
|
33
|
+
Style/FrozenStringLiteralComment:
|
34
|
+
Enabled: False
|
35
|
+
|
36
|
+
Style/MutableConstant:
|
37
|
+
Enabled: False
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
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
|
+
|
15
|
+
## 0.0.3 (2025-04-28)
|
16
|
+
|
17
|
+
### New features
|
18
|
+
|
19
|
+
* Enable Style/BlockComments cop ([@zopolis4][])
|
20
|
+
* Enable Style/WhileUntilDo cop ([@zopolis4][])
|
21
|
+
* Add Crystal/MethodNameStartingWithUppercaseLetter cop ([@zopolis4][])
|
22
|
+
|
23
|
+
### Changes
|
24
|
+
|
25
|
+
* Convert to Rubocop plugin ([@zopolis4][])
|
26
|
+
|
1
27
|
## 0.0.2 (2024-08-01)
|
2
28
|
|
3
29
|
### 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
|
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.
|
@@ -38,5 +38,9 @@ gem install rubocop-crystal
|
|
38
38
|
## Usage
|
39
39
|
|
40
40
|
```
|
41
|
-
rubocop --
|
41
|
+
rubocop --plugin rubocop-crystal
|
42
42
|
```
|
43
|
+
|
44
|
+
Note that there are some differences between Ruby and Crystal that can be automatically resolved, while some (at least for now) require manual intervention.
|
45
|
+
|
46
|
+
If you wish to only process the autocorrectable offenses, add `--disable-uncorrectable`, while reporting only the offenses requiring manual intervention is waiting on rubocop/rubocop#13275.
|
data/config/default.yml
CHANGED
@@ -1,14 +1,43 @@
|
|
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
|
11
28
|
|
29
|
+
Crystal/MethodNameStartingWithUppercaseLetter:
|
30
|
+
Description: 'This cop detects method names that start with uppercase letters.'
|
31
|
+
Enabled: true
|
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
|
+
|
12
41
|
Crystal/RequireRelative:
|
13
42
|
Description: 'This cop replaces require_relative with require while maintaining behavior.'
|
14
43
|
Enabled: true
|
@@ -16,9 +45,15 @@ Crystal/RequireRelative:
|
|
16
45
|
Lint/ImplicitStringConcatenation:
|
17
46
|
Enabled: true
|
18
47
|
|
48
|
+
Style/BlockComments:
|
49
|
+
Enabled: true
|
50
|
+
|
51
|
+
Style/MethodDefParentheses:
|
52
|
+
Enabled: true
|
53
|
+
|
19
54
|
Style/StringLiterals:
|
20
55
|
Enabled: true
|
21
56
|
EnforcedStyle: double_quotes
|
22
57
|
|
23
|
-
Style/
|
58
|
+
Style/WhileUntilDo:
|
24
59
|
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
|
@@ -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)
|
@@ -23,10 +24,10 @@ module RuboCop
|
|
23
24
|
return unless node.source.start_with?("'")
|
24
25
|
# Replace the single quotes deliminating the string with double quotes, and check if the resulting ast is still the same.
|
25
26
|
# If it is, the string doesn't have any interpolation to avoid, and we're done here.
|
26
|
-
return if node == parse(
|
27
|
+
return if node == parse("\"#{node.source[1..-2]}\"").ast
|
27
28
|
|
28
29
|
add_offense(node) do |corrector|
|
29
|
-
corrector.replace(node,
|
30
|
+
corrector.replace(node, "%q(#{node.source[1..-2]})")
|
30
31
|
end
|
31
32
|
end
|
32
33
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Cop
|
3
|
+
module Crystal
|
4
|
+
# Method names cannot start with uppercase letters in Crystal:
|
5
|
+
# https://crystal-lang.org/reference/latest/syntax_and_semantics/methods_and_instance_variables.html
|
6
|
+
# ^ "Method names begin with a lowercase letter and, as a convention, only use lowercase letters, underscores and numbers."
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# def Foo(bar)
|
11
|
+
# qux
|
12
|
+
# end
|
13
|
+
# Foo(bar)
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# def foo(bar)
|
17
|
+
# qux
|
18
|
+
# end
|
19
|
+
# foo(bar)
|
20
|
+
#
|
21
|
+
class MethodNameStartingWithUppercaseLetter < Base
|
22
|
+
MSG = 'Method names must start with a lowercase letter in Crystal.'
|
23
|
+
|
24
|
+
def on_def(node)
|
25
|
+
add_offense(node.loc.name) if node.method_name.to_s.chr.capitalize!.nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -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,16 +19,17 @@ 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
|
|
25
26
|
def on_send(node)
|
26
27
|
add_offense(node) do |corrector|
|
27
|
-
if node.first_argument.value.start_with?('.', '/')
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
require_value = if node.first_argument.value.start_with?('.', '/')
|
29
|
+
node.first_argument.value
|
30
|
+
else
|
31
|
+
"./#{node.first_argument.value}"
|
32
|
+
end
|
32
33
|
corrector.replace(node, "require '#{require_value}'")
|
33
34
|
end
|
34
35
|
end
|
@@ -1,3 +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'
|
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'
|
3
9
|
require_relative 'crystal/require_relative'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'lint_roller'
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Crystal
|
5
|
+
class Plugin < LintRoller::Plugin
|
6
|
+
def about
|
7
|
+
LintRoller::About.new(
|
8
|
+
name: 'rubocop-crystal',
|
9
|
+
version: '0.0.4',
|
10
|
+
homepage: 'https://github.com/Zopolis4/rubocop-crystal',
|
11
|
+
description: 'A RuboCop extension for converting Ruby to Crystal.'
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def supported?(context)
|
16
|
+
context.engine == :rubocop
|
17
|
+
end
|
18
|
+
|
19
|
+
def rules(_context)
|
20
|
+
LintRoller::Rules.new(
|
21
|
+
type: :path,
|
22
|
+
config_format: :rubocop,
|
23
|
+
value: Pathname.new(__dir__).join('../../../config/default.yml')
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/rubocop-crystal.rb
CHANGED
data/rubocop-crystal.gemspec
CHANGED
@@ -1,14 +1,17 @@
|
|
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.
|
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'
|
8
8
|
spec.homepage = 'https://github.com/Zopolis4/rubocop-crystal'
|
9
9
|
|
10
|
+
spec.metadata['default_lint_roller_plugin'] = 'RuboCop::Crystal::Plugin'
|
11
|
+
|
10
12
|
spec.files = `git ls-files`.split("\n")
|
11
13
|
spec.require_paths = ['lib']
|
12
14
|
|
13
|
-
spec.add_dependency '
|
15
|
+
spec.add_dependency 'lint_roller'
|
16
|
+
spec.add_dependency 'rubocop', '>= 1.80.2'
|
14
17
|
end
|