rubocop-elegant 0.0.20 → 0.1.0

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: 68c6e6f486a9804d01f7be2db7087ed14501cec590250e50b89d32511c3cce97
4
- data.tar.gz: f654452715b5e92d85fe3d3e410aca24858a9f28ce25ece59103d66bd3f27582
3
+ metadata.gz: d8341c1ab1fa0a7363197452f41a6146ecff051b7f6f094386612cf3f27b5427
4
+ data.tar.gz: 229010b24b1fbafe803804f3b6615f9295727fe1f8e87bf79902dc9c2a0675d7
5
5
  SHA512:
6
- metadata.gz: af01d8551f274a0c56e69f3caed9097d7db6ffe55d7be9519ba7ea8fc169c6a484eba3e5d5af64e0788eb690d5bd4d155034d60d1d473de6a86bfa4baba480d9
7
- data.tar.gz: f58fb9bbbb83617de532b7ac4712d3089fc61548d43040235cef7bbf46e533a6a5b9d0477b6c2c2124bde2a6b1adcb02eac99f8cec5edcbf0e3831c632e3a620
6
+ metadata.gz: f20ed17a83b4263d2c1336a9d5b1e1d44299c13014fba2a32220df5dd326794cbeda32e15fa1b68474661545b94744d58ed6e8400d894405329a51ad6ca0b965
7
+ data.tar.gz: 79975886caca7e40ac61a08a5a2737b9654c4dd4f43d6633a8a2a0a6047d01c1517094906b2d3920ebccd0e386d5a8afadaa784fc7cf5a5c87d5952f54ab9193
data/README.md CHANGED
@@ -38,7 +38,7 @@ plugins:
38
38
  - rubocop-elegant # must be the last one
39
39
  ```
40
40
 
41
- The `rubocop-elegant` not only provides its own cops, but also configures
41
+ The `rubocop-elegant` plugin not only provides its own cops, but also configures
42
42
  default ones the "right" way.
43
43
 
44
44
  ## How to contribute
data/config/default.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
2
2
  # SPDX-License-Identifier: MIT
3
3
  ---
4
+ # yamllint disable rule:line-length
4
5
  Elegant:
5
6
  Enabled: true
6
7
  DocumentationBaseURL: https://github.com/yegor256/rubocop-elegant
@@ -16,6 +17,28 @@ Elegant/NoEmptyLinesInBlocks:
16
17
  Description: 'Disallows empty lines inside blocks (do/end, if/end, while, case, begin, etc.)'
17
18
  Enabled: true
18
19
  VersionAdded: '0.0.19'
20
+ Elegant/PairedBrackets:
21
+ Description: 'Enforces the paired brackets notation: a bracket must pair on the same line, or start/end its line'
22
+ Enabled: true
23
+ VersionAdded: '0.0.20'
24
+ Elegant/ClassInModule:
25
+ Description: 'Requires every class to be defined inside a module, not globally'
26
+ Enabled: true
27
+ VersionAdded: '0.1.0'
28
+ Exclude:
29
+ - '**/*Test.rb'
30
+ - '**/test_*.rb'
31
+ Elegant/NoClassInModule:
32
+ Description: 'Forbids declaring a class inside a module declaration; use compact namespace syntax instead'
33
+ Enabled: true
34
+ VersionAdded: '0.1.0'
35
+ Exclude:
36
+ - '**/*Test.rb'
37
+ - '**/test_*.rb'
38
+ Elegant/IndentationLadder:
39
+ Description: 'Requires the indentation step to be exactly two spaces when a line indents to the right of the previous one'
40
+ Enabled: true
41
+ VersionAdded: '0.1.0'
19
42
  Elegant/GoodVariableName:
20
43
  Description: 'Checks that variable names match the configured pattern'
21
44
  Enabled: true
@@ -26,7 +49,7 @@ Elegant/GoodMethodName:
26
49
  Description: 'Checks that method names match the configured pattern'
27
50
  Enabled: true
28
51
  VersionAdded: '0.0.3'
29
- Pattern: '^(((to|fake|the|with|without)_)?[a-z]{1,16}[!?]?|(on|test)_[a-z_]+)$'
52
+ Pattern: '^(?=.{1,48}$)(((to|fake|the|with|without)_)?[a-z]{1,16}[!?]?|(on|test)_[a-z_]+)$'
30
53
  AllowedNames: []
31
54
 
32
55
  Style/RedundantException:
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Enforces that every class be defined inside a module. A class declared
7
+ # at the top level, or nested only inside another class, pollutes the
8
+ # global namespace and breaks modular design. The compact namespaced
9
+ # form +class Foo::Bar+ is allowed because its name already resolves
10
+ # into an enclosing namespace.
11
+ class RuboCop::Cop::Elegant::ClassInModule < RuboCop::Cop::Base
12
+ MSG = 'Class %<name>s must be defined inside a module, not globally'
13
+ public_constant :MSG
14
+
15
+ def on_class(node)
16
+ return if namespaced?(node)
17
+ return if scoped?(node)
18
+ add_offense(node, message: format(MSG, name: label(node)))
19
+ end
20
+
21
+ private
22
+
23
+ def namespaced?(node)
24
+ const = node.children[0]
25
+ scope = const.children[0]
26
+ !scope.nil? && scope.type != :cbase
27
+ end
28
+
29
+ def scoped?(node)
30
+ node.each_ancestor(:module).any?
31
+ end
32
+
33
+ def label(node)
34
+ node.children[0].source
35
+ end
36
+ end
@@ -3,41 +3,35 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- module RuboCop
7
- module Cop
8
- module Elegant
9
- class GoodMethodName < Base
10
- MSG = 'Method name "%<name>s" does not match the required pattern'
11
- public_constant :MSG
12
-
13
- def on_def(node)
14
- check(node, node.method_name.to_s)
15
- end
16
-
17
- def on_defs(node)
18
- check(node, node.method_name.to_s)
19
- end
20
-
21
- private
22
-
23
- def check(node, name)
24
- return if allowed?(name)
25
- return if match?(name)
26
- add_offense(node, message: format(MSG, name: name))
27
- end
28
-
29
- def match?(name)
30
- pattern.match?(name)
31
- end
32
-
33
- def allowed?(name)
34
- Array(cop_config['AllowedNames']).map(&:to_s).include?(name)
35
- end
36
-
37
- def pattern
38
- @pattern ||= Regexp.new(cop_config['Pattern'] || '^[a-z]+[!?]?$')
39
- end
40
- end
41
- end
6
+ class RuboCop::Cop::Elegant::GoodMethodName < RuboCop::Cop::Base
7
+ MSG = 'Method name "%<name>s" does not match the required pattern'
8
+ public_constant :MSG
9
+
10
+ def on_def(node)
11
+ check(node, node.method_name.to_s)
12
+ end
13
+
14
+ def on_defs(node)
15
+ check(node, node.method_name.to_s)
16
+ end
17
+
18
+ private
19
+
20
+ def check(node, name)
21
+ return if allowed?(name)
22
+ return if match?(name)
23
+ add_offense(node, message: format(MSG, name: name))
24
+ end
25
+
26
+ def match?(name)
27
+ pattern.match?(name)
28
+ end
29
+
30
+ def allowed?(name)
31
+ Array(cop_config['AllowedNames']).map(&:to_s).include?(name)
32
+ end
33
+
34
+ def pattern
35
+ @pattern ||= Regexp.new(cop_config['Pattern'] || '^[a-z]+[!?]?$')
42
36
  end
43
37
  end
@@ -3,49 +3,43 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- module RuboCop
7
- module Cop
8
- module Elegant
9
- class GoodVariableName < Base
10
- MSG = 'Variable name "%<name>s" does not match the required pattern'
11
- public_constant :MSG
12
-
13
- def on_lvasgn(node)
14
- check(node, node.children.first.to_s)
15
- end
16
-
17
- def on_ivasgn(node)
18
- check(node, node.children.first.to_s)
19
- end
20
-
21
- def on_cvasgn(node)
22
- check(node, node.children.first.to_s)
23
- end
24
-
25
- def on_gvasgn(node)
26
- check(node, node.children.first.to_s)
27
- end
28
-
29
- private
30
-
31
- def check(node, name)
32
- return if allowed?(name)
33
- return if match?(name)
34
- add_offense(node, message: format(MSG, name: name))
35
- end
36
-
37
- def match?(name)
38
- pattern.match?(name)
39
- end
40
-
41
- def allowed?(name)
42
- Array(cop_config['AllowedNames']).map(&:to_s).include?(name)
43
- end
44
-
45
- def pattern
46
- @pattern ||= Regexp.new(cop_config['Pattern'] || '^[a-z]+$')
47
- end
48
- end
49
- end
6
+ class RuboCop::Cop::Elegant::GoodVariableName < RuboCop::Cop::Base
7
+ MSG = 'Variable name "%<name>s" does not match the required pattern'
8
+ public_constant :MSG
9
+
10
+ def on_lvasgn(node)
11
+ check(node, node.children.first.to_s)
12
+ end
13
+
14
+ def on_ivasgn(node)
15
+ check(node, node.children.first.to_s)
16
+ end
17
+
18
+ def on_cvasgn(node)
19
+ check(node, node.children.first.to_s)
20
+ end
21
+
22
+ def on_gvasgn(node)
23
+ check(node, node.children.first.to_s)
24
+ end
25
+
26
+ private
27
+
28
+ def check(node, name)
29
+ return if allowed?(name)
30
+ return if match?(name)
31
+ add_offense(node, message: format(MSG, name: name))
32
+ end
33
+
34
+ def match?(name)
35
+ pattern.match?(name)
36
+ end
37
+
38
+ def allowed?(name)
39
+ Array(cop_config['AllowedNames']).map(&:to_s).include?(name)
40
+ end
41
+
42
+ def pattern
43
+ @pattern ||= Regexp.new(cop_config['Pattern'] || '^[a-z]+$')
50
44
  end
51
45
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Enforces the "indentation ladder" rule: when a line is indented further
7
+ # to the right than the previous non-empty line, the extra indentation
8
+ # must be exactly two spaces. Larger jumps (or odd ones, such as a single
9
+ # space or three spaces) break the visual rhythm of the code and make
10
+ # nesting harder to follow. Lines that match the previous indentation, or
11
+ # de-indent by any amount, are not affected. Lines that belong to the
12
+ # body of a heredoc are ignored, because their whitespace is part of the
13
+ # literal value rather than program structure.
14
+ class RuboCop::Cop::Elegant::IndentationLadder < RuboCop::Cop::Base
15
+ MSG = 'Indentation step of %<step>d spaces is not allowed; use 2 spaces'
16
+ public_constant :MSG
17
+
18
+ def on_new_investigation
19
+ super
20
+ skip = heredocs
21
+ prev = nil
22
+ processed_source.lines.each_with_index do |line, idx|
23
+ num = idx + 1
24
+ next if skip.include?(num)
25
+ next if line.strip.empty?
26
+ indent = line[/\A */].length
27
+ register(num, indent - prev) if prev && indent > prev && (indent - prev) != 2
28
+ prev = indent
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def heredocs
35
+ result = []
36
+ ast = processed_source.ast
37
+ return result if ast.nil?
38
+ ast.each_node(:str, :dstr, :xstr) do |node|
39
+ next unless node.heredoc?
40
+ body = node.loc.heredoc_body
41
+ (body.first_line..body.last_line).each { |num| result << num }
42
+ end
43
+ result
44
+ end
45
+
46
+ def register(num, step)
47
+ target = processed_source.buffer.line_range(num)
48
+ add_offense(target, message: format(MSG, step: step))
49
+ end
50
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Forbids declaring a class inside a module declaration. Instead, the
7
+ # class must be declared with the compact namespace syntax, like
8
+ # +class Foo::Bar+. An empty module +module Foo; end+, a module that
9
+ # contains only other modules, methods, or constants, and a class
10
+ # nested inside another class are all allowed; only a class whose
11
+ # nearest enclosing scope is a module is rejected.
12
+ class RuboCop::Cop::Elegant::NoClassInModule < RuboCop::Cop::Base
13
+ MSG = 'Class %<name>s must use compact namespace syntax, not be nested inside a module'
14
+ public_constant :MSG
15
+
16
+ def on_class(node)
17
+ owner = node.each_ancestor(:module, :class).first
18
+ return if owner.nil? || !owner.module_type?
19
+ add_offense(node, message: format(MSG, name: label(node)))
20
+ end
21
+
22
+ private
23
+
24
+ def label(node)
25
+ node.children[0].source
26
+ end
27
+ end
@@ -3,110 +3,104 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- module RuboCop
7
- module Cop
8
- module Elegant
9
- class NoComments < Base
10
- extend AutoCorrector
11
-
12
- MSG = 'Comment is not allowed, unless it is SPDX, magic, rubocop directive, or docblock'
13
- public_constant :MSG
14
-
15
- def on_new_investigation
16
- processed_source.comments.each do |comment|
17
- register(comment) unless allowed?(comment)
18
- end
19
- end
20
-
21
- private
22
-
23
- def allowed?(comment)
24
- spdx?(comment) || magic?(comment) || rubocop?(comment) || (gemspec? && docblock?(comment))
25
- end
26
-
27
- def spdx?(comment)
28
- comment.text.match?(/^#\s*SPDX-/)
29
- end
30
-
31
- def magic?(comment)
32
- comment.text.match?(/^#\s*(frozen_string_literal|encoding|coding|warn_indent):/)
33
- end
34
-
35
- def rubocop?(comment)
36
- comment.text.match?(/^#\s*rubocop:(disable|enable|todo)\s/)
37
- end
38
-
39
- def gemspec?
40
- return @gemspec if defined?(@gemspec)
41
- path = processed_source.path
42
- return @gemspec = false if path.nil?
43
- @gemspec = root(File.dirname(path))
44
- end
45
-
46
- def root(dir)
47
- return true if Dir.glob(File.join(dir, '*.gemspec')).any?
48
- parent = File.dirname(dir)
49
- return false if parent == dir
50
- root(parent)
51
- end
52
-
53
- def docblock?(comment)
54
- line = comment.location.line
55
- successor = codeline(line)
56
- return false if successor.nil?
57
- definition?(successor)
58
- end
59
-
60
- def codeline(start)
61
- lines = processed_source.lines
62
- (start...lines.size).each do |idx|
63
- content = lines[idx]
64
- next if content.nil?
65
- stripped = content.strip
66
- next if stripped.empty? || stripped.start_with?('#')
67
- return idx + 1
68
- end
69
- nil
70
- end
71
-
72
- def definition?(line)
73
- ast = processed_source.ast
74
- return false if ast.nil?
75
- ast.each_node(:class, :module, :def, :defs) do |node|
76
- return true if node.location.line == line
77
- end
78
- false
79
- end
80
-
81
- def register(comment)
82
- add_offense(comment) do |corrector|
83
- corrector.remove(removal(comment))
84
- end
85
- end
86
-
87
- def removal(comment)
88
- target = comment.source_range
89
- prefix = target.source_line[0, target.column]
90
- return fullrange(target) if prefix.strip.empty?
91
- prefixed(target, prefix)
92
- end
93
-
94
- def fullrange(target)
95
- start = target.begin_pos - target.column
96
- ending = target.end_pos
97
- ending += 1 if newline?(ending)
98
- target.with(begin_pos: start, end_pos: ending)
99
- end
100
-
101
- def prefixed(target, prefix)
102
- spaces = prefix.match(/\s*$/)[0]
103
- target.with(begin_pos: target.begin_pos - spaces.length, end_pos: target.end_pos)
104
- end
105
-
106
- def newline?(pos)
107
- processed_source.buffer.source[pos] == "\n"
108
- end
109
- end
6
+ class RuboCop::Cop::Elegant::NoComments < RuboCop::Cop::Base
7
+ extend RuboCop::Cop::AutoCorrector
8
+
9
+ MSG = 'Comment is not allowed, unless it is SPDX, magic, rubocop directive, or docblock'
10
+ public_constant :MSG
11
+
12
+ def on_new_investigation
13
+ processed_source.comments.each do |comment|
14
+ register(comment) unless allowed?(comment)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def allowed?(comment)
21
+ spdx?(comment) || magic?(comment) || rubocop?(comment) || (gemspec? && docblock?(comment))
22
+ end
23
+
24
+ def spdx?(comment)
25
+ comment.text.match?(/^#\s*SPDX-/)
26
+ end
27
+
28
+ def magic?(comment)
29
+ comment.text.match?(/^#\s*(frozen_string_literal|encoding|coding|warn_indent):/)
30
+ end
31
+
32
+ def rubocop?(comment)
33
+ comment.text.match?(/^#\s*rubocop:(disable|enable|todo)\s/)
34
+ end
35
+
36
+ def gemspec?
37
+ return @gemspec if defined?(@gemspec)
38
+ path = processed_source.path
39
+ return @gemspec = false if path.nil?
40
+ @gemspec = root(File.dirname(path))
41
+ end
42
+
43
+ def root(dir)
44
+ return true if Dir.glob(File.join(dir, '*.gemspec')).any?
45
+ parent = File.dirname(dir)
46
+ return false if parent == dir
47
+ root(parent)
48
+ end
49
+
50
+ def docblock?(comment)
51
+ line = comment.location.line
52
+ successor = codeline(line)
53
+ return false if successor.nil?
54
+ definition?(successor)
55
+ end
56
+
57
+ def codeline(start)
58
+ lines = processed_source.lines
59
+ (start...lines.size).each do |idx|
60
+ content = lines[idx]
61
+ next if content.nil?
62
+ stripped = content.strip
63
+ next if stripped.empty? || stripped.start_with?('#')
64
+ return idx + 1
65
+ end
66
+ nil
67
+ end
68
+
69
+ def definition?(line)
70
+ ast = processed_source.ast
71
+ return false if ast.nil?
72
+ ast.each_node(:class, :module, :def, :defs) do |node|
73
+ return true if node.location.line == line
74
+ end
75
+ false
76
+ end
77
+
78
+ def register(comment)
79
+ add_offense(comment) do |corrector|
80
+ corrector.remove(removal(comment))
110
81
  end
111
82
  end
83
+
84
+ def removal(comment)
85
+ target = comment.source_range
86
+ prefix = target.source_line[0, target.column]
87
+ return fullrange(target) if prefix.strip.empty?
88
+ prefixed(target, prefix)
89
+ end
90
+
91
+ def fullrange(target)
92
+ start = target.begin_pos - target.column
93
+ ending = target.end_pos
94
+ ending += 1 if newline?(ending)
95
+ target.with(begin_pos: start, end_pos: ending)
96
+ end
97
+
98
+ def prefixed(target, prefix)
99
+ spaces = prefix.match(/\s*$/)[0]
100
+ target.with(begin_pos: target.begin_pos - spaces.length, end_pos: target.end_pos)
101
+ end
102
+
103
+ def newline?(pos)
104
+ processed_source.buffer.source[pos] == "\n"
105
+ end
112
106
  end
@@ -3,113 +3,107 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- module RuboCop
7
- module Cop
8
- module Elegant
9
- class NoEmptyLinesInBlocks < Base
10
- extend AutoCorrector
11
-
12
- MSG = 'Empty line inside block body is not allowed'
13
- public_constant :MSG
14
-
15
- def on_new_investigation
16
- super
17
- @reported = []
18
- @gaps = scan
19
- end
20
-
21
- def on_block(node)
22
- check(node)
23
- end
24
-
25
- def on_numblock(node)
26
- check(node)
27
- end
28
-
29
- def on_if(node)
30
- check(node)
31
- end
32
-
33
- def on_while(node)
34
- check(node)
35
- end
36
-
37
- def on_until(node)
38
- check(node)
39
- end
40
-
41
- def on_for(node)
42
- check(node)
43
- end
44
-
45
- def on_case(node)
46
- check(node)
47
- end
48
-
49
- def on_case_match(node)
50
- check(node)
51
- end
52
-
53
- def on_kwbegin(node)
54
- check(node)
55
- end
56
-
57
- private
58
-
59
- def check(node)
60
- lines = range(node)
61
- return if lines.nil?
62
- empty(lines).each { |num| register(num) }
63
- end
64
-
65
- def range(node)
66
- first = node.first_line + 1
67
- last = node.last_line - 1
68
- return if first > last
69
- (first..last)
70
- end
71
-
72
- def empty(lines)
73
- result = []
74
- lines.each do |num|
75
- next if @gaps.include?(num)
76
- line = processed_source.lines[num - 1]
77
- result << num if line.strip.empty?
78
- end
79
- result
80
- end
81
-
82
- def scan
83
- gaps = []
84
- ast = processed_source.ast
85
- return gaps if ast.nil?
86
- ast.each_node(:def, :defs) do |node|
87
- nxt = node.right_sibling
88
- next unless nxt.is_a?(RuboCop::AST::Node)
89
- next unless %i[def defs].include?(nxt.type)
90
- first = node.last_line + 1
91
- last = nxt.first_line - 1
92
- next if first > last
93
- (first..last).each { |n| gaps << n }
94
- end
95
- gaps
96
- end
97
-
98
- def register(num)
99
- return if @reported.include?(num)
100
- @reported << num
101
- target = processed_source.buffer.line_range(num)
102
- add_offense(target) do |corrector|
103
- corrector.remove(fullrange(target))
104
- end
105
- end
106
-
107
- def fullrange(target)
108
- ending = target.end_pos
109
- ending += 1 if processed_source.buffer.source[ending] == "\n"
110
- target.with(end_pos: ending)
111
- end
112
- end
6
+ class RuboCop::Cop::Elegant::NoEmptyLinesInBlocks < RuboCop::Cop::Base
7
+ extend RuboCop::Cop::AutoCorrector
8
+
9
+ MSG = 'Empty line inside block body is not allowed'
10
+ public_constant :MSG
11
+
12
+ def on_new_investigation
13
+ super
14
+ @reported = []
15
+ @gaps = scan
16
+ end
17
+
18
+ def on_block(node)
19
+ check(node)
20
+ end
21
+
22
+ def on_numblock(node)
23
+ check(node)
24
+ end
25
+
26
+ def on_if(node)
27
+ check(node)
28
+ end
29
+
30
+ def on_while(node)
31
+ check(node)
32
+ end
33
+
34
+ def on_until(node)
35
+ check(node)
36
+ end
37
+
38
+ def on_for(node)
39
+ check(node)
40
+ end
41
+
42
+ def on_case(node)
43
+ check(node)
44
+ end
45
+
46
+ def on_case_match(node)
47
+ check(node)
48
+ end
49
+
50
+ def on_kwbegin(node)
51
+ check(node)
52
+ end
53
+
54
+ private
55
+
56
+ def check(node)
57
+ lines = range(node)
58
+ return if lines.nil?
59
+ empty(lines).each { |num| register(num) }
60
+ end
61
+
62
+ def range(node)
63
+ first = node.first_line + 1
64
+ last = node.last_line - 1
65
+ return if first > last
66
+ (first..last)
67
+ end
68
+
69
+ def empty(lines)
70
+ result = []
71
+ lines.each do |num|
72
+ next if @gaps.include?(num)
73
+ line = processed_source.lines[num - 1]
74
+ result << num if line.strip.empty?
113
75
  end
76
+ result
77
+ end
78
+
79
+ def scan
80
+ gaps = []
81
+ ast = processed_source.ast
82
+ return gaps if ast.nil?
83
+ ast.each_node(:def, :defs) do |node|
84
+ nxt = node.right_sibling
85
+ next unless nxt.is_a?(RuboCop::AST::Node)
86
+ next unless %i[def defs].include?(nxt.type)
87
+ first = node.last_line + 1
88
+ last = nxt.first_line - 1
89
+ next if first > last
90
+ (first..last).each { |n| gaps << n }
91
+ end
92
+ gaps
93
+ end
94
+
95
+ def register(num)
96
+ return if @reported.include?(num)
97
+ @reported << num
98
+ target = processed_source.buffer.line_range(num)
99
+ add_offense(target) do |corrector|
100
+ corrector.remove(fullrange(target))
101
+ end
102
+ end
103
+
104
+ def fullrange(target)
105
+ ending = target.end_pos
106
+ ending += 1 if processed_source.buffer.source[ending] == "\n"
107
+ target.with(end_pos: ending)
114
108
  end
115
109
  end
@@ -3,61 +3,55 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- module RuboCop
7
- module Cop
8
- module Elegant
9
- class NoEmptyLinesInMethods < Base
10
- extend AutoCorrector
11
-
12
- MSG = 'Empty line inside method body is not allowed'
13
- public_constant :MSG
14
-
15
- def on_def(node)
16
- check(node)
17
- end
18
-
19
- def on_defs(node)
20
- check(node)
21
- end
22
-
23
- private
24
-
25
- def check(node)
26
- return if node.body.nil?
27
- lines = range(node)
28
- return if lines.nil?
29
- empty(lines).each { |num| register(num) }
30
- end
31
-
32
- def range(node)
33
- first = node.body.first_line
34
- last = node.body.last_line
35
- return if first == last
36
- (first..last)
37
- end
38
-
39
- def empty(lines)
40
- result = []
41
- lines.each do |num|
42
- line = processed_source.lines[num - 1]
43
- result << num if line.strip.empty?
44
- end
45
- result
46
- end
47
-
48
- def register(num)
49
- target = processed_source.buffer.line_range(num)
50
- add_offense(target) do |corrector|
51
- corrector.remove(fullrange(target))
52
- end
53
- end
54
-
55
- def fullrange(target)
56
- ending = target.end_pos
57
- ending += 1 if processed_source.buffer.source[ending] == "\n"
58
- target.with(end_pos: ending)
59
- end
60
- end
6
+ class RuboCop::Cop::Elegant::NoEmptyLinesInMethods < RuboCop::Cop::Base
7
+ extend RuboCop::Cop::AutoCorrector
8
+
9
+ MSG = 'Empty line inside method body is not allowed'
10
+ public_constant :MSG
11
+
12
+ def on_def(node)
13
+ check(node)
14
+ end
15
+
16
+ def on_defs(node)
17
+ check(node)
18
+ end
19
+
20
+ private
21
+
22
+ def check(node)
23
+ return if node.body.nil?
24
+ lines = range(node)
25
+ return if lines.nil?
26
+ empty(lines).each { |num| register(num) }
27
+ end
28
+
29
+ def range(node)
30
+ first = node.body.first_line
31
+ last = node.body.last_line
32
+ return if first == last
33
+ (first..last)
34
+ end
35
+
36
+ def empty(lines)
37
+ result = []
38
+ lines.each do |num|
39
+ line = processed_source.lines[num - 1]
40
+ result << num if line.strip.empty?
41
+ end
42
+ result
43
+ end
44
+
45
+ def register(num)
46
+ target = processed_source.buffer.line_range(num)
47
+ add_offense(target) do |corrector|
48
+ corrector.remove(fullrange(target))
61
49
  end
62
50
  end
51
+
52
+ def fullrange(target)
53
+ ending = target.end_pos
54
+ ending += 1 if processed_source.buffer.source[ending] == "\n"
55
+ target.with(end_pos: ending)
56
+ end
63
57
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Enforces the "paired brackets" notation: every round, square, or curly
7
+ # bracket must either be paired with its matching counterpart on the same
8
+ # line, or end its own line (when opening) or start its own line (when
9
+ # closing). Brackets stranded in the middle of a multi-line expression
10
+ # are forbidden because they hide the structure of the code.
11
+ #
12
+ # See https://www.yegor256.com/2014/10/23/paired-brackets-notation.html
13
+ class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
14
+ MSG = 'Bracket %<text>s must be paired on the same line, or start/end its line'
15
+ public_constant :MSG
16
+
17
+ OPENERS = %i[tLPAREN tLPAREN2 tLPAREN_ARG tLBRACK tLBRACK2 tLCURLY tLBRACE tLBRACE_ARG].freeze
18
+ private_constant :OPENERS
19
+
20
+ CLOSERS = %i[tRPAREN tRBRACK tRCURLY].freeze
21
+ private_constant :CLOSERS
22
+
23
+ def on_new_investigation
24
+ super
25
+ pair.each { |duo| check(duo) }
26
+ end
27
+
28
+ private
29
+
30
+ def pair
31
+ stack = []
32
+ result = []
33
+ processed_source.tokens.each do |tok|
34
+ if OPENERS.include?(tok.type)
35
+ stack << tok
36
+ elsif CLOSERS.include?(tok.type) && !stack.empty?
37
+ result << [stack.pop, tok]
38
+ end
39
+ end
40
+ result
41
+ end
42
+
43
+ def check(duo)
44
+ opener, closer = duo
45
+ return if opener.line == closer.line
46
+ register(opener) unless ends?(opener)
47
+ register(closer) unless starts?(closer)
48
+ end
49
+
50
+ def starts?(tok)
51
+ line = processed_source.lines[tok.line - 1]
52
+ line[0...tok.column].strip.empty?
53
+ end
54
+
55
+ def ends?(tok)
56
+ line = processed_source.lines[tok.line - 1]
57
+ after = line[(tok.column + tok.text.length)..-1].to_s.strip
58
+ after.empty? || after.start_with?('#')
59
+ end
60
+
61
+ def register(tok)
62
+ add_offense(tok.pos, message: format(MSG, text: tok.text))
63
+ end
64
+ end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'elegant/good_method_name'
4
- require_relative 'elegant/good_variable_name'
5
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
6
4
  # SPDX-License-Identifier: MIT
7
5
 
6
+ module RuboCop::Cop::Elegant; end
7
+
8
+ require_relative 'elegant/class_in_module'
9
+ require_relative 'elegant/good_method_name'
10
+ require_relative 'elegant/good_variable_name'
11
+ require_relative 'elegant/indentation_ladder'
12
+ require_relative 'elegant/no_class_in_module'
8
13
  require_relative 'elegant/no_comments'
9
14
  require_relative 'elegant/no_empty_lines_in_blocks'
10
15
  require_relative 'elegant/no_empty_lines_in_methods'
16
+ require_relative 'elegant/paired_brackets'
@@ -5,29 +5,25 @@
5
5
 
6
6
  require 'lint_roller'
7
7
 
8
- module RuboCop
9
- module Elegant
10
- class Plugin < LintRoller::Plugin
11
- def about
12
- LintRoller::About.new(
13
- name: 'rubocop-elegant',
14
- version: VERSION,
15
- homepage: 'https://github.com/yegor256/rubocop-elegant',
16
- description: 'Set of custom RuboCop cops for elegant Ruby coding'
17
- )
18
- end
8
+ class RuboCop::Elegant::Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: 'rubocop-elegant',
12
+ version: RuboCop::Elegant::VERSION,
13
+ homepage: 'https://github.com/yegor256/rubocop-elegant',
14
+ description: 'Set of custom RuboCop cops for elegant Ruby coding'
15
+ )
16
+ end
19
17
 
20
- def supported?(context)
21
- context.engine == :rubocop
22
- end
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
23
21
 
24
- def rules(_context)
25
- LintRoller::Rules.new(
26
- type: :path,
27
- config_format: :rubocop,
28
- value: Pathname.new(__dir__).join('../../../config/default.yml')
29
- )
30
- end
31
- end
22
+ def rules(_context)
23
+ LintRoller::Rules.new(
24
+ type: :path,
25
+ config_format: :rubocop,
26
+ value: Pathname.new(__dir__).join('../../../config/default.yml')
27
+ )
32
28
  end
33
29
  end
@@ -4,6 +4,9 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'rubocop'
7
+
8
+ module RuboCop::Elegant; end
9
+
7
10
  require_relative 'rubocop/cop/elegant_cops'
8
11
  require_relative 'rubocop/elegant/plugin'
9
12
  require_relative 'rubocop/elegant/version'
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
10
10
  s.required_ruby_version = '>=2.2'
11
11
  s.name = 'rubocop-elegant'
12
- s.version = '0.0.20'
12
+ s.version = '0.1.0'
13
13
  s.license = 'MIT'
14
14
  s.summary = 'Set of custom RuboCop cops for elegant Ruby coding'
15
15
  s.description =
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-elegant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.20
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -53,11 +53,15 @@ files:
53
53
  - Rakefile
54
54
  - config/default.yml
55
55
  - lib/rubocop-elegant.rb
56
+ - lib/rubocop/cop/elegant/class_in_module.rb
56
57
  - lib/rubocop/cop/elegant/good_method_name.rb
57
58
  - lib/rubocop/cop/elegant/good_variable_name.rb
59
+ - lib/rubocop/cop/elegant/indentation_ladder.rb
60
+ - lib/rubocop/cop/elegant/no_class_in_module.rb
58
61
  - lib/rubocop/cop/elegant/no_comments.rb
59
62
  - lib/rubocop/cop/elegant/no_empty_lines_in_blocks.rb
60
63
  - lib/rubocop/cop/elegant/no_empty_lines_in_methods.rb
64
+ - lib/rubocop/cop/elegant/paired_brackets.rb
61
65
  - lib/rubocop/cop/elegant_cops.rb
62
66
  - lib/rubocop/elegant/plugin.rb
63
67
  - lib/rubocop/elegant/version.rb