rubocop-elegant 0.2.0 → 0.4.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: d208aee894da49bf76dac6b393ecd907d137ce125f445903078f34def9435955
4
- data.tar.gz: 2bb699562cc741372c3ab5f708a815cc2e046476d3a7646b267c72eb4c49ca98
3
+ metadata.gz: 0441b4d9f29a02a65680ffc46fb56985251723336a076999f7867ad7eb92185d
4
+ data.tar.gz: 92771eff5ed80253a14a85fd96ecaace40e7ce0476f2e26c6cb42372a26b9706
5
5
  SHA512:
6
- metadata.gz: 2c74ed34a71c7fc3b89799f80dc97a77afecf8c2d3d1220b27454b829b27ffd452937a9779a0b17bb562308ef023e9a93ecda0fb6a87c6d10413ef78abf69ca5
7
- data.tar.gz: 472da807a9a7530dcdf5cb5147115b060a379a149d05adf53762134b07314d51d70fe57b855fdf8355a1c0190855a8f1ce59b663a7073ca7f0765aa24f716450
6
+ metadata.gz: b1fd06eba3ce66013593a26400d81698fa3eeb01725890aa3ae5e0f78d086729b6b633f9d6a0551e61e2c1118ad844a12daba2ab97da5306ff13b8feb48a3d2b
7
+ data.tar.gz: 7595e8287a2f7f3904d643c98c1bafc412c2bfebcf4eb2de364ae13af4badf491985eba022f86cf93362efb997d0df57a171068a3ecc1ffb0b27d0c0cbf6e028
data/README.md CHANGED
@@ -18,6 +18,19 @@ Default RuboCop configuration is too permissive, allowing coding practices
18
18
  cryptic variable names, and so on.
19
19
  This plugin configures existing RuboCop cops with stricter settings
20
20
  and adds custom cops for rules that RuboCop doesn't provide out of the box.
21
+ The custom cops add the following restrictions:
22
+
23
+ * Classes must live inside a module; top-level classes are forbidden.
24
+ * A class nested in a module must use compact namespace syntax.
25
+ * Method names must be single lowercase verbs.
26
+ * Variable names must be single lowercase nouns.
27
+ * Each indentation step must add exactly two spaces.
28
+ * Comments are forbidden, except SPDX, magic, RuboCop directives, and class docblocks.
29
+ * Empty lines inside block bodies are forbidden.
30
+ * Empty lines inside method bodies are forbidden.
31
+ * A method cannot return `nil` explicitly.
32
+ * Local variables that are assigned once and read once must be inlined.
33
+ * Brackets must be paired on the same line, or start/end their own line.
21
34
 
22
35
  First, install it:
23
36
 
data/config/default.yml CHANGED
@@ -21,6 +21,10 @@ Elegant/NoNilReturn:
21
21
  Description: 'Forbids a method or function from returning nil on any code path'
22
22
  Enabled: true
23
23
  VersionAdded: '0.1.0'
24
+ Elegant/NoRedundantVariable:
25
+ Description: 'Forbids local variables that are assigned once and read once; such variables must be inlined'
26
+ Enabled: true
27
+ VersionAdded: '0.3.0'
24
28
  Elegant/PairedBrackets:
25
29
  Description: 'Enforces the paired brackets notation: a bracket must pair on the same line, or start/end its line'
26
30
  Enabled: true
@@ -21,8 +21,7 @@ class RuboCop::Cop::Elegant::ClassInModule < RuboCop::Cop::Base
21
21
  private
22
22
 
23
23
  def namespaced?(node)
24
- const = node.children[0]
25
- scope = const.children[0]
24
+ scope = node.children[0].children[0]
26
25
  !scope.nil? && scope.type != :cbase
27
26
  end
28
27
 
@@ -44,7 +44,6 @@ class RuboCop::Cop::Elegant::IndentationLadder < RuboCop::Cop::Base
44
44
  end
45
45
 
46
46
  def register(num, step)
47
- target = processed_source.buffer.line_range(num)
48
- add_offense(target, message: format(MSG, step: step))
47
+ add_offense(processed_source.buffer.line_range(num), message: format(MSG, step: step))
49
48
  end
50
49
  end
@@ -48,8 +48,7 @@ class RuboCop::Cop::Elegant::NoComments < RuboCop::Cop::Base
48
48
  end
49
49
 
50
50
  def docblock?(comment)
51
- line = comment.location.line
52
- successor = codeline(line)
51
+ successor = codeline(comment.location.line)
53
52
  successor.positive? && definition?(successor)
54
53
  end
55
54
 
@@ -88,15 +87,13 @@ class RuboCop::Cop::Elegant::NoComments < RuboCop::Cop::Base
88
87
  end
89
88
 
90
89
  def fullrange(target)
91
- start = target.begin_pos - target.column
92
90
  ending = target.end_pos
93
91
  ending += 1 if newline?(ending)
94
- target.with(begin_pos: start, end_pos: ending)
92
+ target.with(begin_pos: target.begin_pos - target.column, end_pos: ending)
95
93
  end
96
94
 
97
95
  def prefixed(target, prefix)
98
- spaces = prefix.match(/\s*$/)[0]
99
- target.with(begin_pos: target.begin_pos - spaces.length, end_pos: target.end_pos)
96
+ target.with(begin_pos: target.begin_pos - prefix.match(/\s*$/)[0].length, end_pos: target.end_pos)
100
97
  end
101
98
 
102
99
  def newline?(pos)
@@ -70,8 +70,7 @@ class RuboCop::Cop::Elegant::NoEmptyLinesInBlocks < RuboCop::Cop::Base
70
70
  result = []
71
71
  lines.each do |num|
72
72
  next if @gaps.include?(num)
73
- line = processed_source.lines[num - 1]
74
- result << num if line.strip.empty?
73
+ result << num if processed_source.lines[num - 1].strip.empty?
75
74
  end
76
75
  result
77
76
  end
@@ -36,8 +36,7 @@ class RuboCop::Cop::Elegant::NoEmptyLinesInMethods < RuboCop::Cop::Base
36
36
  def empty(lines)
37
37
  result = []
38
38
  lines.each do |num|
39
- line = processed_source.lines[num - 1]
40
- result << num if line.strip.empty?
39
+ result << num if processed_source.lines[num - 1].strip.empty?
41
40
  end
42
41
  result
43
42
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Forbids local variables that are assigned exactly once and then
7
+ # referenced exactly once, since such variables can be inlined at
8
+ # the place of their single use without loss of clarity. The cop
9
+ # inspects each method body in isolation, ignores compound
10
+ # assignments (+=, ||=, &&=), multiple assignments, +for+ loops,
11
+ # rescue variables, and assignments embedded into expressions such
12
+ # as +if+ or +while+ conditions; only top-level statements of a
13
+ # sequence are considered. A variable whose single read sits inside
14
+ # a context that does not also enclose the assignment is left alone:
15
+ # loops or blocks (+block+, +numblock+, +while+, +until+, +for+) so
16
+ # that inlining does not move the right-hand side into a hot path,
17
+ # and conditional branches (+if+, +case+, +case_match+, +and+, +or+,
18
+ # +rescue+, +resbody+, +ensure+) so that inlining does not change
19
+ # whether or when the right-hand side is evaluated. A variable that
20
+ # is reassigned, read more than once, or never read is left alone
21
+ # too.
22
+ #
23
+ # Auto-correct inlines the redundant assignment: it replaces the
24
+ # single +lvar+ read with the source of the assignment's right-hand
25
+ # side, then removes the whole assignment line including its leading
26
+ # indent and trailing newline. The right-hand side is wrapped in
27
+ # parentheses unless it is already a primary expression (literal,
28
+ # variable, parenthesized expression, or method call with parentheses
29
+ # or no arguments), so that operator precedence at the read site is
30
+ # preserved.
31
+ class RuboCop::Cop::Elegant::NoRedundantVariable < RuboCop::Cop::Base
32
+ extend RuboCop::Cop::AutoCorrector
33
+ include RuboCop::Cop::RangeHelp
34
+
35
+ MSG = 'Variable "%<name>s" is redundant and must be inlined: it is read only once'
36
+ public_constant :MSG
37
+
38
+ STATEMENT_PARENTS = %i[begin kwbegin def defs block numblock].freeze
39
+ public_constant :STATEMENT_PARENTS
40
+
41
+ LOOP_TYPES = %i[block numblock while until while_post until_post for].freeze
42
+ public_constant :LOOP_TYPES
43
+
44
+ ALWAYS_FIRST_TYPES = %i[if case case_match and or].freeze
45
+ public_constant :ALWAYS_FIRST_TYPES
46
+
47
+ ALWAYS_HOIST_TYPES = %i[rescue resbody ensure].freeze
48
+ public_constant :ALWAYS_HOIST_TYPES
49
+
50
+ PRIMARY_TYPES = %i[
51
+ int float str sym dstr dsym xstr true false nil array hash regexp
52
+ lvar ivar cvar gvar const self nth_ref back_ref
53
+ ].freeze
54
+ public_constant :PRIMARY_TYPES
55
+
56
+ def on_def(node)
57
+ check(node.body)
58
+ end
59
+
60
+ def on_defs(node)
61
+ check(node.body)
62
+ end
63
+
64
+ private
65
+
66
+ def check(body)
67
+ return if body.nil?
68
+ assigns = Hash.new { |h, k| h[k] = [] }
69
+ reads = Hash.new { |h, k| h[k] = [] }
70
+ tainted = []
71
+ walk(body, assigns, reads, tainted)
72
+ assigns.each do |name, nodes|
73
+ next if tainted.include?(name)
74
+ next unless nodes.size == 1
75
+ next unless reads[name].size == 1
76
+ next if hoisted?(reads[name].first, nodes.first)
77
+ register(nodes.first, reads[name].first, name)
78
+ end
79
+ end
80
+
81
+ def register(assign, read, name)
82
+ return add_offense(assign, message: format(MSG, name: name)) unless solo?(assign)
83
+ add_offense(assign, message: format(MSG, name: name)) do |corrector|
84
+ corrector.replace(read.source_range, inlined(assign.children.last, read))
85
+ corrector.remove(range_by_whole_lines(assign.source_range, include_final_newline: true))
86
+ end
87
+ end
88
+
89
+ def solo?(assign)
90
+ range = assign.source_range
91
+ return false unless range.first_line == range.last_line
92
+ range.source_buffer.source_line(range.first_line).strip == range.source.strip
93
+ end
94
+
95
+ def inlined(rhs, read)
96
+ return "(#{braced(rhs)})" if wrap?(rhs, read)
97
+ braced(rhs)
98
+ end
99
+
100
+ def braced(rhs)
101
+ return "{ #{rhs.source} }" if rhs.hash_type? && rhs.loc.begin.nil?
102
+ rhs.source
103
+ end
104
+
105
+ def wrap?(rhs, read)
106
+ return true unless primary?(rhs)
107
+ rhs.hash_type? && bare?(read)
108
+ end
109
+
110
+ def primary?(node)
111
+ return true if PRIMARY_TYPES.include?(node.type)
112
+ return !node.loc.begin.nil? || node.arguments.empty? if node.send_type? || node.csend_type?
113
+ node.begin_type? && node.children.size == 1
114
+ end
115
+
116
+ def bare?(read)
117
+ parent = read.parent
118
+ return false if parent.nil?
119
+ return false unless parent.send_type? || parent.csend_type?
120
+ parent.loc.begin.nil? && parent.arguments.include?(read)
121
+ end
122
+
123
+ def walk(node, assigns, reads, tainted)
124
+ return unless node.is_a?(RuboCop::AST::Node)
125
+ return if node.def_type? || node.defs_type?
126
+ record(node, assigns, reads, tainted)
127
+ node.each_child_node { |child| walk(child, assigns, reads, tainted) }
128
+ end
129
+
130
+ def record(node, assigns, reads, tainted)
131
+ if node.op_asgn_type? || node.or_asgn_type? || node.and_asgn_type?
132
+ taint(node.children.first, tainted)
133
+ elsif node.lvasgn_type? && statement?(node)
134
+ assigns[node.children.first] << node
135
+ elsif node.lvar_type?
136
+ reads[node.children.first] << node
137
+ end
138
+ end
139
+
140
+ def taint(target, tainted)
141
+ return unless target.is_a?(RuboCop::AST::Node)
142
+ return unless target.lvasgn_type?
143
+ tainted << target.children.first
144
+ end
145
+
146
+ def statement?(node)
147
+ return false unless node.children.size == 2
148
+ parent = node.parent
149
+ parent = parent.parent while !parent.nil? && parent.type == :begin && parent.children.size == 1
150
+ return false if parent.nil?
151
+ STATEMENT_PARENTS.include?(parent.type)
152
+ end
153
+
154
+ def hoisted?(read, assign)
155
+ boundary = assign.parent
156
+ return true if boundary.nil?
157
+ child = read
158
+ parent = read.parent
159
+ while !parent.nil? && !parent.equal?(boundary)
160
+ return true if LOOP_TYPES.include?(parent.type)
161
+ return true if ALWAYS_HOIST_TYPES.include?(parent.type)
162
+ return true if ALWAYS_FIRST_TYPES.include?(parent.type) && !parent.children.first.equal?(child)
163
+ child = parent
164
+ parent = parent.parent
165
+ end
166
+ parent.nil?
167
+ end
168
+ end
@@ -9,8 +9,17 @@
9
9
  # closing). Brackets stranded in the middle of a multi-line expression
10
10
  # are forbidden because they hide the structure of the code.
11
11
  #
12
+ # Auto-correct relocates the offending bracket onto its own line: an
13
+ # opener that is not at end-of-line gets a newline and the opener-line
14
+ # indent plus two spaces inserted right after it; a closer that is not
15
+ # at start-of-line gets a newline and the opener-line indent inserted
16
+ # right before it. Surrounding indentation may still need a follow-up
17
+ # layout pass, but the brackets themselves end up paired.
18
+ #
12
19
  # See https://www.yegor256.com/2014/10/23/paired-brackets-notation.html
13
20
  class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
21
+ extend RuboCop::Cop::AutoCorrector
22
+
14
23
  MSG = 'Bracket %<text>s must be paired on the same line, or start/end its line'
15
24
  public_constant :MSG
16
25
 
@@ -43,22 +52,25 @@ class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
43
52
  def check(duo)
44
53
  opener, closer = duo
45
54
  return if opener.line == closer.line
46
- register(opener) unless ends?(opener)
47
- register(closer) unless starts?(closer)
55
+ indent = leading(opener)
56
+ register(opener) { |corrector| corrector.insert_after(opener.pos, "\n#{indent} ") } unless ends?(opener)
57
+ register(closer) { |corrector| corrector.insert_before(closer.pos, "\n#{indent}") } unless starts?(closer)
48
58
  end
49
59
 
50
60
  def starts?(tok)
51
- line = processed_source.lines[tok.line - 1]
52
- line[0...tok.column].strip.empty?
61
+ processed_source.lines[tok.line - 1][0...tok.column].strip.empty?
53
62
  end
54
63
 
55
64
  def ends?(tok)
56
- line = processed_source.lines[tok.line - 1]
57
- after = line[(tok.column + tok.text.length)..-1].to_s.strip
65
+ after = processed_source.lines[tok.line - 1][(tok.column + tok.text.length)..-1].to_s.strip
58
66
  after.empty? || after.start_with?('#')
59
67
  end
60
68
 
61
- def register(tok)
62
- add_offense(tok.pos, message: format(MSG, text: tok.text))
69
+ def leading(tok)
70
+ processed_source.lines[tok.line - 1][/\A[ \t]*/]
71
+ end
72
+
73
+ def register(tok, &block)
74
+ add_offense(tok.pos, message: format(MSG, text: tok.text), &block)
63
75
  end
64
76
  end
@@ -14,4 +14,5 @@ require_relative 'elegant/no_comments'
14
14
  require_relative 'elegant/no_empty_lines_in_blocks'
15
15
  require_relative 'elegant/no_empty_lines_in_methods'
16
16
  require_relative 'elegant/no_nil_return'
17
+ require_relative 'elegant/no_redundant_variable'
17
18
  require_relative 'elegant/paired_brackets'
@@ -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.2.0'
12
+ s.version = '0.4.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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -62,6 +62,7 @@ files:
62
62
  - lib/rubocop/cop/elegant/no_empty_lines_in_blocks.rb
63
63
  - lib/rubocop/cop/elegant/no_empty_lines_in_methods.rb
64
64
  - lib/rubocop/cop/elegant/no_nil_return.rb
65
+ - lib/rubocop/cop/elegant/no_redundant_variable.rb
65
66
  - lib/rubocop/cop/elegant/paired_brackets.rb
66
67
  - lib/rubocop/cop/elegant_cops.rb
67
68
  - lib/rubocop/elegant/plugin.rb