rubocop-elegant 0.7.0 → 0.8.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: caefc6549542af9deff3c876e4f5820c45b1641a0006d797c05776123675a3d2
4
- data.tar.gz: 59a23203e472ff8485a69e714f3085efa6a5a323381097f4fedb104df5363238
3
+ metadata.gz: 25c022ecd592d9b71642d845716275362e0175a24c1703046fa987dd5fac977b
4
+ data.tar.gz: 7e8df712bdc9326743e2baa33db7117742759f8bb685e42403ffce8b537b7b31
5
5
  SHA512:
6
- metadata.gz: 553939a2d42896140b0b778faa879bdf4729a5411a60c22013b66313173d2b2785d5c7052bf68baf8ec2b223d450de7707393a73aa7a0a47bfa970283ae388ab
7
- data.tar.gz: 8dbae33a180941efead52d5ed739f0f1ee349947769790d322a8ece2e224169ef79179fed362383f508bd05d5ab27b84935e3a552028b04a132c0077a3ee2a28
6
+ metadata.gz: 80858ff6896e4509151c4e6df75df9dc7559fa08154cd5b4ef890a57bcf6b3a9b33bb64ec59f98d8ec45dad7bcfd02a94402cd3b9d0943ac581d66e5e94be718
7
+ data.tar.gz: e7555aa1fb5a013dd33ed84959ff6e5bee69df1dd433625a48ab9a148159c9b525a3b9244807c5f769c6fa2ac68d705b085c85a1f4a91a6371618752a1e507fe
data/README.md CHANGED
@@ -32,6 +32,7 @@ The custom cops add the following restrictions:
32
32
  * A method cannot return `nil` explicitly.
33
33
  * Local variables that are assigned once and read once must be inlined.
34
34
  * Brackets must be paired on the same line, or start/end their own line.
35
+ * The SPDX header must sit above the code, with only comments before it.
35
36
 
36
37
  First, install it:
37
38
 
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rake'
4
- require 'rake/clean'
5
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
6
4
  # SPDX-License-Identifier: MIT
7
5
 
6
+ require 'rake'
7
+ require 'rake/clean'
8
8
  require 'rubygems'
9
9
 
10
10
  CLEAN.include('coverage')
data/config/default.yml CHANGED
@@ -36,6 +36,10 @@ Elegant/ClassInModule:
36
36
  Exclude:
37
37
  - '**/*Test.rb'
38
38
  - '**/test_*.rb'
39
+ - '**/*_test.rb'
40
+ - '**/*_spec.rb'
41
+ - 'test/**/*'
42
+ - 'spec/**/*'
39
43
  Elegant/NoClassInModule:
40
44
  Description: 'Forbids declaring a class inside a module declaration; use compact namespace syntax instead'
41
45
  Enabled: true
@@ -43,6 +47,10 @@ Elegant/NoClassInModule:
43
47
  Exclude:
44
48
  - '**/*Test.rb'
45
49
  - '**/test_*.rb'
50
+ - '**/*_test.rb'
51
+ - '**/*_spec.rb'
52
+ - 'test/**/*'
53
+ - 'spec/**/*'
46
54
  Elegant/OneClassPerFile:
47
55
  Description: 'Allows only one non-empty top-level class per file; empty forward declarations are treated as namespace scaffolding'
48
56
  Enabled: true
@@ -50,6 +58,10 @@ Elegant/OneClassPerFile:
50
58
  Exclude:
51
59
  - '**/*Test.rb'
52
60
  - '**/test_*.rb'
61
+ - '**/*_test.rb'
62
+ - '**/*_spec.rb'
63
+ - 'test/**/*'
64
+ - 'spec/**/*'
53
65
  Elegant/MonotonicIndents:
54
66
  Description: 'Requires the indentation step to be exactly two spaces when a line indents to the right of the previous one'
55
67
  Enabled: true
@@ -66,6 +78,10 @@ Elegant/GoodMethodName:
66
78
  VersionAdded: '0.0.3'
67
79
  Pattern: '^(?=.{1,48}$)(((to|fake|the|with|without)_)?[a-z]{1,16}[!?]?|(on|test)_[a-z_]+)$'
68
80
  AllowedNames: []
81
+ Elegant/SpdxOnTop:
82
+ Description: 'Requires the SPDX header to sit above the code, with only comments and blank lines before it'
83
+ Enabled: true
84
+ VersionAdded: '0.8.0'
69
85
 
70
86
  Style/RedundantException:
71
87
  Enabled: false
@@ -78,11 +94,19 @@ Metrics/ModuleLength:
78
94
  Exclude:
79
95
  - '**/*Test.rb'
80
96
  - '**/test_*.rb'
97
+ - '**/*_test.rb'
98
+ - '**/*_spec.rb'
99
+ - 'test/**/*'
100
+ - 'spec/**/*'
81
101
  Metrics/ClassLength:
82
102
  Max: 300
83
103
  Exclude:
84
104
  - '**/*Test.rb'
85
105
  - '**/test_*.rb'
106
+ - '**/*_test.rb'
107
+ - '**/*_spec.rb'
108
+ - 'test/**/*'
109
+ - 'spec/**/*'
86
110
  Metrics/MethodLength:
87
111
  Max: 100
88
112
  Metrics/BlockLength:
@@ -103,6 +127,8 @@ Layout/EndOfLine:
103
127
  EnforcedStyle: lf
104
128
  Layout/ParameterAlignment:
105
129
  Enabled: false
130
+ Layout/ArrayAlignment:
131
+ EnforcedStyle: with_fixed_indentation
106
132
  Layout/LineLength:
107
133
  Max: 120
108
134
  Style/MultilineBlockChain:
@@ -4,10 +4,13 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
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.
7
+ # at the top level pollutes the global namespace and breaks modular
8
+ # design. The compact namespaced form +class Foo::Bar+ is allowed
9
+ # because its name already resolves into an enclosing namespace, and so
10
+ # is a class nested inside another class, since the enclosing class
11
+ # names it too: +class Foo::Bar; class Baz+ defines +Foo::Bar::Baz+.
12
+ # A class declared with an explicit root scope, +class ::Baz+, is global
13
+ # whatever encloses it lexically, so it is reported wherever it sits.
11
14
  class RuboCop::Cop::Elegant::ClassInModule < RuboCop::Cop::Base
12
15
  MSG = 'Class %<name>s must be defined inside a module, not globally'
13
16
  public_constant :MSG
@@ -21,12 +24,17 @@ class RuboCop::Cop::Elegant::ClassInModule < RuboCop::Cop::Base
21
24
  private
22
25
 
23
26
  def namespaced?(node)
24
- scope = node.children[0].children[0]
27
+ scope = outer(node)
25
28
  !scope.nil? && scope.type != :cbase
26
29
  end
27
30
 
28
31
  def scoped?(node)
29
- node.each_ancestor(:module).any?
32
+ return false unless outer(node).nil?
33
+ node.each_ancestor(:module, :class).any?
34
+ end
35
+
36
+ def outer(node)
37
+ node.children[0].children[0]
30
38
  end
31
39
 
32
40
  def label(node)
@@ -18,11 +18,16 @@ class RuboCop::Cop::Elegant::GoodMethodName < RuboCop::Cop::Base
18
18
  private
19
19
 
20
20
  def check(node, name)
21
+ return unless identifier?(name)
21
22
  return if allowed?(name)
22
23
  return if match?(name)
23
24
  add_offense(node, message: format(MSG, name: name))
24
25
  end
25
26
 
27
+ def identifier?(name)
28
+ /\A[a-zA-Z_]/.match?(name)
29
+ end
30
+
26
31
  def match?(name)
27
32
  pattern.match?(name)
28
33
  end
@@ -23,7 +23,10 @@
23
23
  # Auto-correct inlines the redundant assignment: it replaces the
24
24
  # single +lvar+ read with the source of the assignment's right-hand
25
25
  # side, then removes the whole assignment line including its leading
26
- # indent and trailing newline. The right-hand side is wrapped in
26
+ # indent and trailing newline. When the read is the value of a Ruby
27
+ # 3.1 shorthand pair, such as the +ids+ of +call(ids:)+, key and
28
+ # value share one range, so the whole pair is rewritten into its
29
+ # long form +ids: <rhs>+ instead. The right-hand side is wrapped in
27
30
  # parentheses unless it is already a primary expression (literal,
28
31
  # variable, parenthesized expression, or method call with parentheses
29
32
  # or no arguments), so that operator precedence at the read site is
@@ -81,11 +84,26 @@ class RuboCop::Cop::Elegant::NoRedundantVariable < RuboCop::Cop::Base
81
84
  def register(assign, read, name)
82
85
  return add_offense(assign, message: format(MSG, name: name)) unless solo?(assign)
83
86
  add_offense(assign, message: format(MSG, name: name)) do |corrector|
84
- corrector.replace(read.source_range, inlined(assign.children.last, read))
87
+ corrector.replace(*rewrite(assign.children.last, read))
85
88
  corrector.remove(range_by_whole_lines(assign.source_range, include_final_newline: true))
86
89
  end
87
90
  end
88
91
 
92
+ def rewrite(rhs, read)
93
+ pair = shorthand(read)
94
+ return [read.source_range, inlined(rhs, read)] if pair.nil?
95
+ [pair.source_range, "#{pair.children.first.source}: #{inlined(rhs, read)}"]
96
+ end
97
+
98
+ def shorthand(read)
99
+ parent = read.parent
100
+ return if parent.nil? || !parent.pair_type?
101
+ key, value = parent.children
102
+ return unless value.equal?(read)
103
+ return unless key.source_range == value.source_range
104
+ parent
105
+ end
106
+
89
107
  def solo?(assign)
90
108
  range = assign.source_range
91
109
  return false unless range.first_line == range.last_line
@@ -10,11 +10,14 @@
10
10
  # are forbidden because they hide the structure of the code.
11
11
  #
12
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.
13
+ # opener that is not at end-of-line is followed by a newline and the
14
+ # opener-line indent plus two spaces; a closer that is not at
15
+ # start-of-line is preceded by a newline and the opener-line indent.
16
+ # The whitespace already sitting next to the bracket is consumed by
17
+ # the rewrite rather than left in place, so the moved code lands on
18
+ # the intended column instead of drifting right by that much.
19
+ # Surrounding indentation may still need a follow-up layout pass, but
20
+ # the brackets themselves end up paired.
18
21
  #
19
22
  # See https://www.yegor256.com/2014/10/23/paired-brackets-notation.html
20
23
  class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
@@ -29,6 +32,9 @@ class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
29
32
  CLOSERS = %i[tRPAREN tRBRACK tRCURLY].freeze
30
33
  private_constant :CLOSERS
31
34
 
35
+ BLANKS = [' ', "\t"].freeze
36
+ private_constant :BLANKS
37
+
32
38
  def on_new_investigation
33
39
  super
34
40
  pair.each { |duo| check(duo) }
@@ -53,8 +59,22 @@ class RuboCop::Cop::Elegant::PairedBrackets < RuboCop::Cop::Base
53
59
  opener, closer = duo
54
60
  return if opener.line == closer.line
55
61
  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)
62
+ register(opener) { |corrector| corrector.replace(trailing(opener), "\n#{indent} ") } unless ends?(opener)
63
+ register(closer) { |corrector| corrector.replace(preceding(closer), "\n#{indent}") } unless starts?(closer)
64
+ end
65
+
66
+ def trailing(tok)
67
+ text = processed_source.buffer.source
68
+ stop = tok.pos.end_pos
69
+ stop += 1 while BLANKS.include?(text[stop])
70
+ tok.pos.end.resize(stop - tok.pos.end_pos)
71
+ end
72
+
73
+ def preceding(tok)
74
+ text = processed_source.buffer.source
75
+ from = tok.pos.begin_pos
76
+ from -= 1 while from.positive? && BLANKS.include?(text[from - 1])
77
+ tok.pos.begin.adjust(begin_pos: from - tok.pos.begin_pos)
58
78
  end
59
79
 
60
80
  def starts?(tok)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ # Requires the SPDX header to sit above the code. The header is the
7
+ # license declaration of the file, and REUSE keeps it in the first
8
+ # few lines so that scanners, editors and reviewers read the license
9
+ # without parsing past unrelated code. Only blank lines and comments
10
+ # may precede it: a shebang, a magic comment, a RuboCop directive.
11
+ # A file that carries no SPDX comment at all is left alone, because
12
+ # checking that the tags exist is the job of +reuse lint+ and not of
13
+ # this plugin. Only the first SPDX comment counts, so the tags of a
14
+ # multi-line header may be split by nothing but comments.
15
+ class RuboCop::Cop::Elegant::SpdxOnTop < RuboCop::Cop::Base
16
+ MSG = 'SPDX header must be above the code, but this line comes before it'
17
+ public_constant :MSG
18
+
19
+ def on_new_investigation
20
+ header = spdx
21
+ return if header.nil?
22
+ line = intruder(header.location.line)
23
+ return if line.nil?
24
+ add_offense(processed_source.buffer.line_range(line))
25
+ end
26
+
27
+ private
28
+
29
+ def spdx
30
+ processed_source.comments.find { |comment| comment.text.match?(/^#\s*SPDX-/) }
31
+ end
32
+
33
+ def intruder(stop)
34
+ (1...stop).find { |idx| code?(processed_source.lines[idx - 1]) }
35
+ end
36
+
37
+ def code?(line)
38
+ text = line.strip
39
+ !text.empty? && !text.start_with?('#')
40
+ end
41
+ end
@@ -17,3 +17,4 @@ require_relative 'elegant/no_nil_return'
17
17
  require_relative 'elegant/no_redundant_variable'
18
18
  require_relative 'elegant/one_class_per_file'
19
19
  require_relative 'elegant/paired_brackets'
20
+ require_relative 'elegant/spdx_on_top'
@@ -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.7.0'
12
+ s.version = '0.8.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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -65,6 +65,7 @@ files:
65
65
  - lib/rubocop/cop/elegant/no_redundant_variable.rb
66
66
  - lib/rubocop/cop/elegant/one_class_per_file.rb
67
67
  - lib/rubocop/cop/elegant/paired_brackets.rb
68
+ - lib/rubocop/cop/elegant/spdx_on_top.rb
68
69
  - lib/rubocop/cop/elegant_cops.rb
69
70
  - lib/rubocop/elegant/plugin.rb
70
71
  - lib/rubocop/elegant/version.rb