rubocop-fourshark 0.8.6 → 0.10.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: 1bdc31121f34ef40cca25af1bc804caafecc8b8f58fd2996457cd01d82b98502
4
- data.tar.gz: 26bbc30201fe22eef3477a20ce1eaf7016ee020a1ed93f53134429654da4a11d
3
+ metadata.gz: 6c6c6460f92a176876caceeea69665b587c8df4502c09ed2ac8472113a0cabbb
4
+ data.tar.gz: 3b5ce9b992dce38aa64163363b2495b1dc1f9a6af2373d2b23b296e3f9242169
5
5
  SHA512:
6
- metadata.gz: ce23cde85c85d07932802b8bc89aa7c48f047cd274f59ff7d8c32b5e5e490e4bc46f118bb0e6216796cc774f9dc7c687bc464d33ac59361885a8bab2dd865edb
7
- data.tar.gz: 616ed09b7f636b5cc4931894d86063a62b9e7357cf35802dee67f6d4a2c77ab8523131519ced29806cb6e1bd862ce8771931401411b3b1e0d59ebe6fabc5b575
6
+ metadata.gz: d4c6340581cd762875dbdeead991ea6d6f6ddadf420a5261345ef396ba8b1ca6bedd148d411c7b061615486cfbfb44e84983acd940f5d0ab632d99af97c2100c
7
+ data.tar.gz: bfc42021a9aba8b72ecdded7a31f6965bd9eb6d31408762e9d2621cfc60597df16c81ede7f9b9c6b9112f80587c7b62e3cff2d7d02c331345f2ea64934733d0e
data/.grant.yaml ADDED
@@ -0,0 +1,28 @@
1
+ # License allow-list enforced by `grant` over the Syft SBOM (see .github/workflows/main.yml).
2
+ # Mirrors the licenses the 4Shark Ruby projects permit. require-license/require-known-license
3
+ # are false so a dependency with no declared license or a non-SPDX license string still passes.
4
+ allow:
5
+ - MIT
6
+ - Apache-2.0
7
+ - "BSD-2-Clause"
8
+ - "BSD-3-Clause"
9
+ - "BSD"
10
+ - "BSD 2-Clause"
11
+ - ISC
12
+ - Ruby
13
+ - ruby
14
+ - "same as ruby's"
15
+ - Zlib
16
+ - "LGPL-2.1*"
17
+ - "LGPL-3.0*"
18
+ - MPL-2.0
19
+ - "Artistic-*"
20
+ - "Brakeman Public Use License"
21
+ - "GPL-2.0*"
22
+ - "GPL-3.0*"
23
+ - GPLv2
24
+ - WTFPL
25
+ - "Prosperity*"
26
+
27
+ require-license: false
28
+ require-known-license: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.5
1
+ 4.0.6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ ## [0.10.0] - 2026-10-02
2
+
3
+ ### Added
4
+
5
+ - License compliance verification
6
+
7
+ ### Changed
8
+
9
+ - Gem update pull requests limited to the named gem
10
+ - Minimum release age verification across lockfiles and manifests
11
+ - License check fails at a failed tool install
12
+ - `Style/DisallowDelegate` accepts a constant the object supplies to the collaborator
13
+
14
+ ## [0.9.0] - 2026-08-31
15
+
16
+ ### Added
17
+
18
+ - `Layout/EmptyLineBeforeGuardClause` — a guard clause that follows ordinary code must be preceded by a blank line
19
+ - Automated dependency-update runs
20
+ - Minimum-age verification on pull requests, re-evaluated daily
21
+ - `Layout/SingleLineStatementSpacing` — a blank line between two consecutive single-line statements is removed
22
+ - `Style/OrderedConstants` — a run of consecutive constant assignments must be sorted alphabetically
23
+
24
+ ### Changed
25
+
26
+ - Development Ruby version to 4.0.6
27
+
28
+ ### Fixed
29
+
30
+ - Weekly transitive dependency updates
31
+ - Transitive dependency pull request now covers every lockfile the update touched
32
+ - Lockfile discovery no longer publishes a short set when the search itself fails
33
+ - Transitive resolution no longer runs on a short lockfile set when the search itself fails
34
+ - Lockfile discovery skips the bundles vendored under node_modules
35
+ - Held-back gem list pinned to the collation that sorted it
36
+ - Daily dependency age re-check leaving pull requests without the status they need to merge
37
+ - Dependency age checks running with no configured minimum
38
+ - Minimum release age applied to the weekly transitive dependency update
39
+ - Ruby setup action pinned like every other action in the workflows
40
+ - Minimum-age check no longer errors on the gem's own unreleased version
41
+
1
42
  ## [0.8.6] - 2026-08-08
2
43
 
3
44
  ### Fixed
data/README.md CHANGED
@@ -80,12 +80,15 @@ Where a 4Shark cop supersedes or contradicts a stock cop, `config/default.yml` t
80
80
  | `Style/DisallowSafeNavigation` | Flags safe navigation (`&.`). 4Shark rejects it — a `&.` chain silently swallows a `nil` that usually signals a real bug. Use an explicit conditional so the `nil` case is handled on purpose. |
81
81
  | `Style/DisallowTernary` | Flags the ternary conditional (`cond ? a : b`). `?` and `:` are punctuation that already mean other things in Ruby (`valid?`, `:symbol`), and the ternary hides a branch inside an expression where skimming misses it. Use an explicit `if`/`else`. |
82
82
  | `Style/DisallowTry` | Flags `try` / `try!`. Same rationale — it hides the `nil`/missing-method case instead of handling it explicitly. |
83
+ | `Style/OrderedConstants` | A run of consecutive constant assignments must be sorted alphabetically by name. A lone constant is never flagged, and any non-constant statement breaks the run. Flags without autocorrecting — a constant can reference one declared above it, so a human decides the order. Experimental. |
83
84
 
84
85
  ### Layout
85
86
 
86
87
  | Cop | Intent |
87
88
  |---|---|
89
+ | `Layout/EmptyLineBeforeGuardClause` | Requires a blank line before a guard clause that follows ordinary code, so a clause that breaks the flow is findable at a glance instead of read for. Consecutive guard clauses are one block and stay glued together — only the first of a run needs the blank line, and a clause that opens a body needs none. Mirrors the stock `Layout/EmptyLineAfterGuardClause`, which sets the run off below; enable both. |
88
90
  | `Layout/MultilineStatementSpacing` | Requires a blank line between two consecutive statements when either spans multiple lines, so multi-line statements read as distinct units. |
91
+ | `Layout/SingleLineStatementSpacing` | Forbids a blank line between two consecutive single-line statements — they are one run, so the blank reveals nothing. The complement of `Layout/MultilineStatementSpacing`; the two never fire on the same pair. Autocorrects by removing the blank. |
89
92
 
90
93
  ### Naming
91
94
 
data/Rakefile CHANGED
@@ -9,9 +9,7 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
9
9
  end
10
10
 
11
11
  RuboCop::RakeTask.new
12
-
13
12
  task default: %i[spec rubocop]
14
-
15
13
  desc 'Generate a new cop with a template'
16
14
 
17
15
  task :new_cop, [:cop] do |_task, args|
@@ -23,11 +21,9 @@ task :new_cop, [:cop] do |_task, args|
23
21
  end
24
22
 
25
23
  generator = RuboCop::Cop::Generator.new(cop_name)
26
-
27
24
  generator.write_source
28
25
  generator.write_spec
29
26
  generator.inject_require(root_file_path: 'lib/rubocop/cop/fourshark_cops.rb')
30
27
  generator.inject_config(config_file_path: 'config/default.yml')
31
-
32
28
  puts generator.todo
33
29
  end
data/config/default.yml CHANGED
@@ -3,11 +3,25 @@
3
3
  # so the names read like any other cop. Where a 4Shark cop supersedes or
4
4
  # contradicts a stock cop, the stock cop is disabled here (see the bottom block).
5
5
 
6
+ # Mirrors the stock `Layout/EmptyLineAfterGuardClause` rather than replacing it:
7
+ # that one sets the run off below, this one above, and both are wanted.
8
+ Layout/EmptyLineBeforeGuardClause:
9
+ Description: 'Add a blank line before a guard clause that follows ordinary code.'
10
+ Enabled: true
11
+ VersionAdded: '0.9.0'
12
+
6
13
  Layout/MultilineStatementSpacing:
7
14
  Description: 'Add a blank line between consecutive statements when either spans multiple lines.'
8
15
  Enabled: true
9
16
  VersionAdded: '0.2.0'
10
17
 
18
+ # The complement of Layout/MultilineStatementSpacing: no blank line between two
19
+ # single-line statements. Autocorrects (removing a blank has no placement ambiguity).
20
+ Layout/SingleLineStatementSpacing:
21
+ Description: 'Do not put a blank line between two consecutive single-line statements.'
22
+ Enabled: true
23
+ VersionAdded: '0.9.0'
24
+
11
25
  # Stock cop configured (not a 4Shark cop): enforce the `indented` style for
12
26
  # multiline method-call chains — continuation calls are indented one step from
13
27
  # the receiver instead of aligning with the first call.
@@ -42,6 +56,11 @@ Style/DisallowTry:
42
56
  Enabled: true
43
57
  VersionAdded: '0.1.0'
44
58
 
59
+ Style/OrderedConstants:
60
+ Description: 'Sort a run of consecutive constant assignments alphabetically by name.'
61
+ Enabled: true
62
+ VersionAdded: '0.9.0'
63
+
45
64
  Rails/MandatoryInverseOf:
46
65
  Description: 'Every association must declare `inverse_of` (stricter than stock `Rails/InverseOf`).'
47
66
  Enabled: true
@@ -23,16 +23,16 @@ module RuboCop
23
23
  # end
24
24
  #
25
25
  class AssociationInFactory < ::RuboCop::Cop::Base
26
- MSG = 'Do not declare associations in factories — set them manually in the spec.'
27
-
28
26
  CONTAINER_METHODS = %i[factory trait].freeze
29
27
  # FactoryBot DSL methods that are bare calls but are NOT associations.
30
28
  DSL_KEYWORDS = %i[skip_create initialize_with].freeze
29
+ MSG = 'Do not declare associations in factories — set them manually in the spec.'
31
30
 
32
31
  def on_block(node)
33
32
  return unless container_block?(node)
34
33
 
35
34
  body = node.body
35
+
36
36
  return unless body
37
37
 
38
38
  statements = if body.begin_type?
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'factory_bot/association_in_factory'
4
+ require_relative 'layout/empty_line_before_guard_clause'
4
5
  require_relative 'layout/multiline_statement_spacing'
6
+ require_relative 'layout/single_line_statement_spacing'
5
7
  require_relative 'rails/bidirectional_association'
6
8
  require_relative 'rails/mandatory_inverse_of'
7
9
  require_relative 'rails/optional_belongs_to'
@@ -14,3 +16,4 @@ require_relative 'style/disallow_delegate'
14
16
  require_relative 'style/disallow_safe_navigation'
15
17
  require_relative 'style/disallow_ternary'
16
18
  require_relative 'style/disallow_try'
19
+ require_relative 'style/ordered_constants'
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Layout
8
+ # Requires a blank line before a guard clause that follows ordinary code.
9
+ # A guard clause breaks the flow, so it has to be findable at a glance
10
+ # rather than read for — the same reason a multi-line statement is set off
11
+ # from its neighbours.
12
+ #
13
+ # Consecutive guard clauses are one block and stay glued together: the run
14
+ # is what the reader scans, and blank lines inside it would break the shape
15
+ # instead of revealing it. Only the first clause of a run needs the blank
16
+ # line above, and a guard that opens a body needs none because the `def`
17
+ # or `do` already delimits it.
18
+ #
19
+ # What counts as a guard clause is `guard_clause?` from `rubocop-ast`,
20
+ # which covers `return`, `break`, `next`, `raise` and `fail`.
21
+ #
22
+ # The blank line *after* the run is the stock
23
+ # `Layout/EmptyLineAfterGuardClause`, which this cop mirrors rather than
24
+ # replaces — enable both to get a run that is set off on both sides.
25
+ #
26
+ # Detection-only (no autocorrect), matching the sibling
27
+ # `Layout/MultilineStatementSpacing`: where a comment sits above the
28
+ # clause the blank line belongs above the comment, and that placement
29
+ # needs validation against the real repos before it is applied for anyone.
30
+ #
31
+ # @example
32
+ # # bad
33
+ # def foo
34
+ # bar
35
+ # return if baz?
36
+ #
37
+ # qux
38
+ # end
39
+ #
40
+ # # good
41
+ # def foo
42
+ # bar
43
+ #
44
+ # return if baz?
45
+ #
46
+ # qux
47
+ # end
48
+ #
49
+ # # good — a run of guard clauses stays together
50
+ # def foo
51
+ # return if baz?
52
+ # return if qux?
53
+ #
54
+ # bar
55
+ # end
56
+ #
57
+ # # good — nothing above it to separate from
58
+ # def foo
59
+ # return if baz?
60
+ #
61
+ # bar
62
+ # end
63
+ #
64
+ class EmptyLineBeforeGuardClause < ::RuboCop::Cop::Base
65
+ MSG = 'Add a blank line before the guard clause.'
66
+
67
+ def on_begin(node)
68
+ node.children.each_cons(2) do |previous, current|
69
+ next unless guard_clause?(current)
70
+ next if guard_clause?(previous)
71
+ next if blank_line_between?(previous, current)
72
+
73
+ add_offense(current)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def guard_clause?(node)
80
+ return false unless node.is_a?(::RuboCop::AST::Node)
81
+ return false unless node.if_type?
82
+
83
+ branch = node.if_branch
84
+
85
+ return false if branch.nil?
86
+
87
+ branch.guard_clause?
88
+ end
89
+
90
+ def blank_line_between?(previous, current)
91
+ return false if current.first_line - previous.last_line < 2
92
+
93
+ ((previous.last_line + 1)...current.first_line).any? do |line|
94
+ processed_source.lines[line - 1].strip.empty?
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Layout
8
+ # Forbids a blank line between two consecutive single-line statements —
9
+ # they are one run, so the blank reveals nothing. A blank that separates
10
+ # code from an adjacent comment is kept, as is one owned by another cop
11
+ # (a guard clause, a flow-control statement, an access modifier) or one
12
+ # beside a multi-line or heredoc neighbour. Autocorrects by removing it.
13
+ class SingleLineStatementSpacing < ::RuboCop::Cop::Base
14
+ extend ::RuboCop::Cop::AutoCorrector
15
+ ACCESS_MODIFIERS = %i[private protected public module_function].freeze
16
+ FLOW_CONTROL_METHODS = %i[raise fail throw].freeze
17
+ FLOW_CONTROL_TYPES = %i[return next break redo retry].freeze
18
+ MSG = 'Remove the blank line between consecutive single-line statements.'
19
+
20
+ def on_begin(node)
21
+ node.children.each_cons(2) do |first, second|
22
+ next unless first.is_a?(::RuboCop::AST::Node) && second.is_a?(::RuboCop::AST::Node)
23
+ next if structural?(first) || structural?(second)
24
+
25
+ blanks = removable_blank_lines(first, second)
26
+
27
+ next if blanks.empty?
28
+
29
+ add_offense(second) do |corrector|
30
+ blanks.each { |line| corrector.remove(line_range_with_newline(line)) }
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # A statement whose surrounding blank another cop owns, or which reads as
38
+ # a block. Its blank is preserved: this cop only glues ordinary runs.
39
+ def structural?(node)
40
+ multiline_or_heredoc?(node) || flow_control?(node) || guard_clause?(node) || access_modifier?(node)
41
+ end
42
+
43
+ def multiline_or_heredoc?(node)
44
+ node.multiline? || node.each_node(:any_str).any?(&:heredoc?)
45
+ end
46
+
47
+ def flow_control?(node)
48
+ return true if FLOW_CONTROL_TYPES.include?(node.type)
49
+
50
+ node.send_type? && node.receiver.nil? && FLOW_CONTROL_METHODS.include?(node.method_name)
51
+ end
52
+
53
+ def guard_clause?(node)
54
+ return false unless node.if_type?
55
+
56
+ branch = node.if_branch
57
+
58
+ return false if branch.nil?
59
+
60
+ branch.guard_clause?
61
+ end
62
+
63
+ def access_modifier?(node)
64
+ node.send_type? && node.receiver.nil? && ACCESS_MODIFIERS.include?(node.method_name)
65
+ end
66
+
67
+ # A blank whose nearest non-blank line on each side is the same kind
68
+ # (code-to-code or comment-to-comment). A blank on a code/comment border
69
+ # separates the two deliberately and is left alone.
70
+ def removable_blank_lines(first, second)
71
+ gap = (first.last_line + 1)...second.first_line
72
+
73
+ gap.select do |line|
74
+ blank_line?(line) && same_kind_neighbours?(line, first.last_line, second.first_line)
75
+ end
76
+ end
77
+
78
+ def same_kind_neighbours?(line, floor, ceiling)
79
+ above = nearest_non_blank(line - 1, floor, -1)
80
+ below = nearest_non_blank(line + 1, ceiling, 1)
81
+ line_kind(above) == line_kind(below)
82
+ end
83
+
84
+ def nearest_non_blank(start_line, boundary, step)
85
+ line = start_line
86
+ line += step while line != boundary && blank_line?(line)
87
+ line
88
+ end
89
+
90
+ def line_kind(line)
91
+ return :comment if processed_source.lines[line - 1].strip.start_with?('#')
92
+
93
+ :code
94
+ end
95
+
96
+ def blank_line?(line)
97
+ processed_source.lines[line - 1].strip.empty?
98
+ end
99
+
100
+ def line_range_with_newline(line)
101
+ line_range = processed_source.buffer.line_range(line)
102
+ line_range.resize(line_range.length + 1)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -43,6 +43,7 @@ module RuboCop
43
43
  next if model_name.nil? || target_class.nil?
44
44
 
45
45
  opposite_model_path = File.join(Dir.pwd, "app/models/#{camel_to_snake(target_class)}.rb")
46
+
46
47
  next unless File.exist?(opposite_model_path)
47
48
 
48
49
  opposite_content = File.read(opposite_model_path)
@@ -81,7 +82,6 @@ module RuboCop
81
82
  assoc_name = literal_value(node.first_argument)
82
83
  inverse_name = extract_inverse_of(node)
83
84
  target_class = extract_class_name(node, assoc_name)
84
-
85
85
  [model_name, assoc_name, inverse_name, target_class]
86
86
  end
87
87
  end
@@ -107,6 +107,7 @@ module RuboCop
107
107
  # plain symbol (string keys, double-splats) are skipped rather than read.
108
108
  def option_pair(node, key)
109
109
  kwargs = node.last_argument
110
+
110
111
  return nil unless kwargs.respond_to?(:hash_type?) && kwargs.hash_type?
111
112
 
112
113
  kwargs.pairs.find { |pair| pair.key.sym_type? && pair.key.value == key }
@@ -125,6 +126,7 @@ module RuboCop
125
126
 
126
127
  def class_name_from_ast
127
128
  class_node = processed_source.ast.each_descendant(:class).first
129
+
128
130
  return nil unless class_node
129
131
 
130
132
  const_node = class_node.children.first
@@ -38,6 +38,7 @@ module RuboCop
38
38
 
39
39
  def optional_true?(node)
40
40
  kwargs = node.last_argument
41
+
41
42
  return false if !kwargs || !kwargs.hash_type?
42
43
 
43
44
  kwargs.pairs.any? { |pair| pair.key.value == :optional && pair.value.true_type? }
@@ -27,9 +27,8 @@ module RuboCop
27
27
  # validates :name
28
28
  #
29
29
  class OrderedMacros < ::RuboCop::Cop::Base
30
- MSG = 'Sort `%<macro>s` declarations alphabetically (`%<name>s` should come before `%<previous>s`).'
31
-
32
30
  MACROS = %i[belongs_to has_one has_many has_and_belongs_to_many validates scope].freeze
31
+ MSG = 'Sort `%<macro>s` declarations alphabetically (`%<name>s` should come before `%<previous>s`).'
33
32
 
34
33
  def self.default_configuration
35
34
  super.merge('Include' => ['app/models/**/*.rb'])
@@ -37,6 +36,7 @@ module RuboCop
37
36
 
38
37
  def on_class(node)
39
38
  body = node.body
39
+
40
40
  return unless body
41
41
 
42
42
  statements = if body.begin_type?
@@ -68,6 +68,7 @@ module RuboCop
68
68
  calls.each_cons(2) do |previous, current|
69
69
  previous_name = macro_name(previous)
70
70
  current_name = macro_name(current)
71
+
71
72
  next if current_name >= previous_name
72
73
 
73
74
  add_offense(
@@ -25,14 +25,14 @@ module RuboCop
25
25
  # end
26
26
  #
27
27
  class ConditionalInLet < ::RuboCop::Cop::Base
28
- MSG = 'Do not put conditional logic in a `let` — use separate contexts.'
29
-
30
28
  LET_METHODS = %i[let let!].freeze
29
+ MSG = 'Do not put conditional logic in a `let` — use separate contexts.'
31
30
 
32
31
  def on_block(node)
33
32
  return unless let_block?(node)
34
33
 
35
34
  body = node.body
35
+
36
36
  return unless body
37
37
 
38
38
  body.each_node(:if, :case) do |conditional|
@@ -17,9 +17,8 @@ module RuboCop
17
17
  # let(:user) { create(:user) }
18
18
  #
19
19
  class FactoryBotInBefore < ::RuboCop::Cop::Base
20
- MSG = 'Do not create objects in `before` — use `let` for object creation.'
21
-
22
20
  CREATION_METHODS = %i[create build create_list build_list].freeze
21
+ MSG = 'Do not create objects in `before` — use `let` for object creation.'
23
22
 
24
23
  def on_block(node)
25
24
  return unless before_block?(node)
@@ -20,8 +20,8 @@ module RuboCop
20
20
  # model class (a linter runs without the Rails app booted).
21
21
  #
22
22
  class InverseOfMatcher < ::RuboCop::Cop::Base
23
- MSG_MISSING_INVERSE = 'Root models must include `.inverse_of` in association specs.'
24
23
  MSG_FORBIDDEN_INVERSE = 'Subclasses must NOT include `.inverse_of` in specs (it belongs to the parent).'
24
+ MSG_MISSING_INVERSE = 'Root models must include `.inverse_of` in association specs.'
25
25
 
26
26
  def self.default_configuration
27
27
  super.merge(
@@ -33,12 +33,15 @@ module RuboCop
33
33
  return unless node.method?(:belong_to)
34
34
 
35
35
  model_name = model_class_from_spec
36
+
36
37
  return unless model_name
37
38
 
38
39
  association_name = association_name_from(node)
40
+
39
41
  return if association_name && polymorphic_in_model?(model_name, association_name)
40
42
 
41
43
  classification = classify_model(model_name)
44
+
42
45
  return if classification == :non_ar
43
46
 
44
47
  if classification == :root
@@ -66,6 +69,7 @@ module RuboCop
66
69
  # The association name passed to the matcher, e.g. belong_to(:user) -> :user
67
70
  def association_name_from(node)
68
71
  argument = node.first_argument
72
+
69
73
  return nil unless argument && argument.sym_type?
70
74
 
71
75
  argument.value
@@ -76,6 +80,7 @@ module RuboCop
76
80
  # and the declaration lives in the model, not in the spec matcher chain.
77
81
  def polymorphic_in_model?(model_name, association_name)
78
82
  content = model_source(model_name)
83
+
79
84
  return false unless content
80
85
 
81
86
  content.match?(/belongs_to\s+:#{Regexp.escape(association_name.to_s)}\b[^\n]*polymorphic:\s*true/)
@@ -84,12 +89,15 @@ module RuboCop
84
89
  # Convert spec file path into model class name
85
90
  def model_class_from_spec
86
91
  path = processed_source.file_path if processed_source && processed_source.buffer
92
+
87
93
  return nil unless path
88
94
 
89
95
  match = path.match(%r{spec/models/(.+)_spec\.rb})
96
+
90
97
  return nil unless match
91
98
 
92
99
  relative = match[1]
100
+
93
101
  return nil if relative.empty?
94
102
 
95
103
  relative.split('/').map { |part| part.split('_').map(&:capitalize).join }.join('::')
@@ -99,9 +107,11 @@ module RuboCop
99
107
  # model file statically (no class loading).
100
108
  def classify_model(model_name)
101
109
  content = model_source(model_name)
110
+
102
111
  return :non_ar unless content
103
112
 
104
113
  superclass = superclass_of(content, model_name)
114
+
105
115
  return :non_ar unless superclass
106
116
  return :root if superclass == 'ApplicationRecord'
107
117
  return :subclass if model_source(superclass)
@@ -113,6 +123,7 @@ module RuboCop
113
123
 
114
124
  def model_source(model_name)
115
125
  path = File.join(Dir.pwd, 'app/models', "#{camel_to_snake(model_name)}.rb")
126
+
116
127
  return File.read(path) if File.exist?(path)
117
128
 
118
129
  nil
@@ -32,18 +32,19 @@ module RuboCop
32
32
  # end
33
33
  #
34
34
  class OverwrittenLet < ::RuboCop::Cop::Base
35
- MSG = 'Do not override the outer `let` `%<name>s`; use a distinct name or set the value in `before`.'
36
-
37
- LET_METHODS = %i[let let!].freeze
38
35
  EXAMPLE_GROUP_METHODS = %i[describe context].freeze
36
+ LET_METHODS = %i[let let!].freeze
37
+ MSG = 'Do not override the outer `let` `%<name>s`; use a distinct name or set the value in `before`.'
39
38
 
40
39
  def on_send(node)
41
40
  return unless LET_METHODS.include?(node.method_name) && node.receiver.nil?
42
41
 
43
42
  name = let_name(node)
43
+
44
44
  return unless name
45
45
 
46
46
  immediate_group = node.each_ancestor(:block).find { |block| example_group?(block) }
47
+
47
48
  return unless immediate_group
48
49
  return unless outer_groups(immediate_group).any? { |group| defines_let?(group, name) }
49
50
 
@@ -63,6 +64,7 @@ module RuboCop
63
64
 
64
65
  def defines_let?(group, name)
65
66
  body = group.body
67
+
66
68
  return false unless body
67
69
 
68
70
  statements = if body.begin_type?
@@ -92,6 +94,7 @@ module RuboCop
92
94
 
93
95
  def let_name(node)
94
96
  first = node.first_argument
97
+
95
98
  return first.value if first && first.sym_type?
96
99
 
97
100
  nil
@@ -44,10 +44,10 @@ module RuboCop
44
44
  # contributes something the caller would otherwise have to reach in and
45
45
  # take, and the result is a simpler API on the object that owns the data.
46
46
  #
47
- # Own state is an instance variable, `self`, or a receiverless call,
48
- # reached directly, through a wrapper, or through a call on it — `record`,
49
- # `record.owner_id` and `[record.owner_id]` all carry the object's own
50
- # data. A method PARAMETER does not: it arrives as an `lvar`, so a setter
47
+ # Own state is an instance variable, `self`, a receiverless call, or a
48
+ # constant, reached directly, through a wrapper, or through a call on it —
49
+ # `record.owner_id` and `Settings.keep_alive` both carry data the object
50
+ # chose to supply. A method PARAMETER does not: it arrives as an `lvar`, so a setter
51
51
  # handing its argument to a collaborator stays flagged.
52
52
  #
53
53
  # Two limits keep the exemption from swallowing the rule.
@@ -120,15 +120,13 @@ module RuboCop
120
120
  # end
121
121
  #
122
122
  class DisallowDelegate < ::RuboCop::Cop::Base
123
- MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
124
- FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
125
-
126
- RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
127
-
128
123
  # An argument reaches the body wrapped in one of these when the call site
129
124
  # spreads or blocks it, and the wrapper says nothing about whose state it
130
125
  # carries.
131
126
  ARGUMENT_WRAPPERS = %i[hash pair array splat kwsplat block_pass].freeze
127
+ FORWARDER_MSG = 'Do not forward a collaborator\'s message. Delete the forwarder and let the caller navigate.'
128
+ MACRO_MSG = 'Do not use automatic delegation. Delete the forwarder and let the caller navigate.'
129
+ RESTRICT_ON_SEND = %i[delegate delegate_missing_to def_delegator def_delegators DelegateClass].freeze
132
130
 
133
131
  # @!method mixes_in_enumerable?(node)
134
132
  def_node_matcher :mixes_in_enumerable?, <<~PATTERN
@@ -170,6 +168,7 @@ module RuboCop
170
168
  # same thing and both are read as one.
171
169
  def own_class_target?(node)
172
170
  target = to_option(node) || leading_target(node)
171
+
173
172
  return false if target.nil?
174
173
  return false unless target.type?(:sym, :str)
175
174
 
@@ -199,6 +198,7 @@ module RuboCop
199
198
  return false unless node.method?(:each)
200
199
 
201
200
  enclosing = node.each_ancestor(:class, :module, :sclass).first
201
+
202
202
  return false if enclosing.nil?
203
203
  return false if enclosing.sclass_type?
204
204
 
@@ -224,8 +224,8 @@ module RuboCop
224
224
  end
225
225
 
226
226
  # A method parameter reaches the body as an `lvar`, so only an instance
227
- # variable, `self`, or a receiverless call counts as the object's own
228
- # state — read directly, through a wrapper, or through a call on it.
227
+ # variable, `self`, a receiverless call, or a constant counts as the
228
+ # object's own state — read directly, through a wrapper, or through a call on it.
229
229
  # Anything rooted at the collaborator is that collaborator's own data
230
230
  # coming back, which is the echo the rule forbids. The first guard
231
231
  # refuses anything that is not a node, which covers both the nil child
@@ -236,6 +236,7 @@ module RuboCop
236
236
  return false if argument == collaborator
237
237
  return true if argument.ivar_type?
238
238
  return true if argument.self_type?
239
+ return true if argument.const_type?
239
240
  return own_state?(argument.receiver, collaborator) if call_with_receiver?(argument)
240
241
  return true if argument.send_type?
241
242
  return argument.children.any? { |child| own_state?(child, collaborator) } if wrapper?(argument)
@@ -7,7 +7,6 @@ module RuboCop
7
7
  module Style
8
8
  class DisallowTry < ::RuboCop::Cop::Base
9
9
  MSG = 'Do not use `try` or `try!`. Use explicit conditionals instead.'
10
-
11
10
  RESTRICT_ON_SEND = %i[try try!].freeze
12
11
 
13
12
  def on_send(node)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Style
8
+ # Requires a run of consecutive constant assignments to be sorted
9
+ # alphabetically by name. A lone constant is never flagged; a run is
10
+ # broken by any non-constant statement. Flags without autocorrecting — a
11
+ # constant may reference one declared above it, so a human decides the
12
+ # order. Experimental: if it flags more than it helps, drop it.
13
+ class OrderedConstants < ::RuboCop::Cop::Base
14
+ MSG = 'Sort constant assignments alphabetically (`%<name>s` should come before `%<previous>s`).'
15
+
16
+ def on_begin(node)
17
+ node.children.chunk { |child| constant_assignment?(child) }.each do |constants, run|
18
+ flag_unsorted(run) if constants
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def flag_unsorted(run)
25
+ run.each_cons(2) do |previous, current|
26
+ previous_name = constant_name(previous)
27
+ current_name = constant_name(current)
28
+
29
+ next if current_name >= previous_name
30
+
31
+ add_offense(current.loc.name, message: format(MSG, name: current_name, previous: previous_name))
32
+ end
33
+ end
34
+
35
+ def constant_assignment?(node)
36
+ node.is_a?(::RuboCop::AST::Node) && node.casgn_type?
37
+ end
38
+
39
+ def constant_name(node)
40
+ node.name.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Fourshark
5
- VERSION = '0.8.6'
5
+ VERSION = '0.10.0'
6
6
  end
7
7
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubocop'
4
-
5
4
  require_relative 'rubocop/fourshark'
6
5
  require_relative 'rubocop/fourshark/version'
7
6
  require_relative 'rubocop/fourshark/plugin'
8
-
9
7
  require_relative 'rubocop/cop/fourshark_cops'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-fourshark
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Ribeiro
@@ -127,6 +127,7 @@ executables: []
127
127
  extensions: []
128
128
  extra_rdoc_files: []
129
129
  files:
130
+ - ".grant.yaml"
130
131
  - ".ruby-gemset"
131
132
  - ".ruby-version"
132
133
  - CHANGELOG.md
@@ -138,7 +139,9 @@ files:
138
139
  - lib/rubocop-fourshark.rb
139
140
  - lib/rubocop/cop/factory_bot/association_in_factory.rb
140
141
  - lib/rubocop/cop/fourshark_cops.rb
142
+ - lib/rubocop/cop/layout/empty_line_before_guard_clause.rb
141
143
  - lib/rubocop/cop/layout/multiline_statement_spacing.rb
144
+ - lib/rubocop/cop/layout/single_line_statement_spacing.rb
142
145
  - lib/rubocop/cop/rails/bidirectional_association.rb
143
146
  - lib/rubocop/cop/rails/mandatory_inverse_of.rb
144
147
  - lib/rubocop/cop/rails/optional_belongs_to.rb
@@ -151,6 +154,7 @@ files:
151
154
  - lib/rubocop/cop/style/disallow_safe_navigation.rb
152
155
  - lib/rubocop/cop/style/disallow_ternary.rb
153
156
  - lib/rubocop/cop/style/disallow_try.rb
157
+ - lib/rubocop/cop/style/ordered_constants.rb
154
158
  - lib/rubocop/fourshark.rb
155
159
  - lib/rubocop/fourshark/plugin.rb
156
160
  - lib/rubocop/fourshark/version.rb
@@ -177,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
181
  - !ruby/object:Gem::Version
178
182
  version: '0'
179
183
  requirements: []
180
- rubygems_version: 4.0.10
184
+ rubygems_version: 4.0.16
181
185
  specification_version: 4
182
186
  summary: RuboCop extension enforcing 4Shark's Ruby, Rails, and RSpec conventions.
183
187
  test_files: []