rubocop-discourse 3.14.0 → 3.17.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: e722863ce072ee0a4d928e09b36a1e12e3b653618fdc0fe70a2f417d466e0ffa
4
- data.tar.gz: bd4a868a26bd7df10e3196c9d9c419f1b4268051d6302b335994eb539ae6e61e
3
+ metadata.gz: 6c45b71e5356cb68292653738d05f27653a5e922d62442130751e8c341506680
4
+ data.tar.gz: 89e1f0b302c37bf300ceb97cea9a2035e49811c33e8d32dc50c22972f128f563
5
5
  SHA512:
6
- metadata.gz: 0e266174ecceb19102b630bdb01419b0b22580fcadaf5cc5f69a3ba7c7c91a6274ff391a7cf43769c71cb035699e1867d96b0c72ba28cd0706335a5821ce3fe4
7
- data.tar.gz: 3e5d42d6aa34bf2eb23daaf91eef254623cd83fe812525c2bda429cd96e9f3ec0b0d0b19129d3faa5e64e749decf0164f012d99755787559d2e715ec3e16441c
6
+ metadata.gz: fa562f62898674a35e4d7d12c513d648727fa20d982812ed4e7af1a30e08b65e6249fd1f20cd250f1a9e422dd15509b7409bec1ed1a9ac4897dedd5e56e1b0e7
7
+ data.tar.gz: 2cf1e4dd9cdaf5a57dfe7d3814c2624c46b3179b93ce5b9341be33e763270bb2f5ffd3ebb66ff74c86e269a4e23c27081b02232a40822d2689c1089e79378a0a
data/config/default.yml CHANGED
@@ -56,6 +56,11 @@ Discourse/NoMixingMultisiteAndStandardSpecs:
56
56
  Include:
57
57
  - "**/spec/**/*"
58
58
 
59
+ Discourse/NoSystemSpecMetadata:
60
+ Enabled: true
61
+ Include:
62
+ - "**/spec/**/*"
63
+
59
64
  Discourse/Plugins/CallRequiresPlugin:
60
65
  Enabled: true
61
66
  Include:
@@ -89,3 +94,6 @@ Discourse/Services/GroupKeywords:
89
94
 
90
95
  Discourse/Services/EmptyLinesAroundBlocks:
91
96
  Enabled: true
97
+
98
+ Discourse/Services/MutableAttributeDefault:
99
+ Enabled: true
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ # System spec metadata is inferred from the file path, so explicit
7
+ # `type: :system` and `system: true` metadata on `RSpec.describe` is redundant.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # RSpec.describe "login", system: true do
12
+ # end
13
+ #
14
+ # # good
15
+ # RSpec.describe "login" do
16
+ # end
17
+ class NoSystemSpecMetadata < Base
18
+ extend AutoCorrector
19
+
20
+ MSG = "Remove redundant `type: :system` and `system: true` metadata from `RSpec.describe`."
21
+ RESTRICT_ON_SEND = %i[describe].freeze
22
+
23
+ def_node_matcher :describe?, <<~PATTERN
24
+ (send
25
+ {nil? (const nil? :RSpec)}
26
+ :describe
27
+ ...
28
+ )
29
+ PATTERN
30
+
31
+ def_node_matcher :system_type_pair?, <<~PATTERN
32
+ (pair (sym :type) (sym :system))
33
+ PATTERN
34
+
35
+ def_node_matcher :system_true_pair?, <<~PATTERN
36
+ (pair (sym :system) true)
37
+ PATTERN
38
+
39
+ def on_send(node)
40
+ return unless describe?(node)
41
+ return unless node.last_argument&.hash_type?
42
+
43
+ hash = node.last_argument
44
+ offending_pairs =
45
+ hash.pairs.select { |pair| system_type_pair?(pair) || system_true_pair?(pair) }
46
+
47
+ return if offending_pairs.empty?
48
+
49
+ add_offense(offending_pairs.first) do |corrector|
50
+ corrector.replace(node, corrected_send_source(node, hash, offending_pairs))
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def corrected_send_source(node, hash, offending_pairs)
57
+ remaining_pairs = hash.pairs - offending_pairs
58
+ before_hash = source_for(node.source_range.begin_pos, hash.source_range.begin_pos)
59
+ after_hash = source_for(hash.source_range.end_pos, node.source_range.end_pos)
60
+
61
+ return "#{before_hash.sub(/,\s*\z/m, "")}#{after_hash}" if remaining_pairs.empty?
62
+
63
+ "#{before_hash}#{corrected_hash_source(hash, remaining_pairs)}#{after_hash}"
64
+ end
65
+
66
+ def corrected_hash_source(hash, remaining_pairs)
67
+ body =
68
+ remaining_pairs
69
+ .each_with_index
70
+ .map do |pair, index|
71
+ separator = index.zero? ? "" : pair_separator(hash, pair)
72
+ "#{separator}#{pair.source}"
73
+ end
74
+ .join
75
+
76
+ return body unless hash.braces?
77
+
78
+ return "{ #{body} }" unless hash.multiline?
79
+
80
+ indentation = " " * remaining_pairs.first.loc.expression.column
81
+ closing_indentation = " " * hash.loc.end.column
82
+
83
+ "{\n#{indentation}#{body}\n#{closing_indentation}}"
84
+ end
85
+
86
+ def pair_separator(hash, pair)
87
+ return ", " unless hash.multiline?
88
+
89
+ ",\n#{" " * pair.loc.expression.column}"
90
+ end
91
+
92
+ def source_for(begin_pos, end_pos)
93
+ processed_source.buffer.source[begin_pos...end_pos]
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Discourse
6
+ module Services
7
+ # Wrap mutable `attribute` defaults in a proc — `ActiveModel::Attributes`
8
+ # shares the literal across instances, so mutations leak between calls.
9
+ # `:array`-typed attributes are exempt; the type dups on read.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # options do
14
+ # attribute :overrides, default: {}
15
+ # attribute :ids, default: [1, 2]
16
+ # end
17
+ #
18
+ # # good
19
+ # options do
20
+ # attribute :overrides, default: -> { {} }
21
+ # attribute :ids, default: -> { [1, 2] }
22
+ # attribute :tags, :array, default: [] # :array dups per read
23
+ # end
24
+ #
25
+ class MutableAttributeDefault < Base
26
+ extend AutoCorrector
27
+
28
+ MSG =
29
+ "Mutable `default: %<source>s` is shared across all instances; wrap it in a proc: `-> { %<source>s }`."
30
+ RESTRICT_ON_SEND = %i[attribute].freeze
31
+
32
+ def_node_matcher :service_include?, <<~MATCHER
33
+ (class _ _
34
+ {
35
+ (begin <(send nil? :include (const (const nil? :Service) :Base)) ...>)
36
+ <(send nil? :include (const (const nil? :Service) :Base)) ...>
37
+ }
38
+ )
39
+ MATCHER
40
+
41
+ def_node_matcher :mutable_default, <<~PATTERN
42
+ (send nil? :attribute _ ... (hash <(pair (sym :default) ${hash array}) ...>))
43
+ PATTERN
44
+
45
+ def_node_matcher :array_typed?, <<~PATTERN
46
+ (send nil? :attribute _ (sym :array) ...)
47
+ PATTERN
48
+
49
+ def on_class(node)
50
+ @service = true if service_include?(node)
51
+ end
52
+
53
+ def on_send(node)
54
+ return unless @service
55
+ return if array_typed?(node)
56
+ default = mutable_default(node)
57
+ return unless default
58
+
59
+ source = default.source
60
+ add_offense(default, message: format(MSG, source: source)) do |corrector|
61
+ corrector.replace(default, "-> { #{source} }")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Discourse
5
- VERSION = "3.14.0"
5
+ VERSION = "3.17.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-discourse
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.14.0
4
+ version: 3.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Discourse Team
@@ -167,6 +167,7 @@ files:
167
167
  - lib/rubocop/cop/discourse/no_mocking_jobs.rb
168
168
  - lib/rubocop/cop/discourse/no_nokogiri_html_fragment.rb
169
169
  - lib/rubocop/cop/discourse/no_reset_column_information_in_migrations.rb
170
+ - lib/rubocop/cop/discourse/no_system_spec_metadata.rb
170
171
  - lib/rubocop/cop/discourse/no_time_new_without_args.rb
171
172
  - lib/rubocop/cop/discourse/no_uri_escape_encode.rb
172
173
  - lib/rubocop/cop/discourse/only_top_level_multisite_specs.rb
@@ -178,6 +179,7 @@ files:
178
179
  - lib/rubocop/cop/discourse/plugins/use_require_relative.rb
179
180
  - lib/rubocop/cop/discourse/services/empty_lines_around_blocks.rb
180
181
  - lib/rubocop/cop/discourse/services/group_keywords.rb
182
+ - lib/rubocop/cop/discourse/services/mutable_attribute_default.rb
181
183
  - lib/rubocop/cop/discourse/time_eq_matcher.rb
182
184
  - lib/rubocop/cop/discourse_cops.rb
183
185
  - lib/rubocop/discourse/plugin.rb