betterlint 1.1.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 530a4f9e133d2b476275f42d7f58050eab86379908d1d7b1ebc0ac56c7c680f5
4
- data.tar.gz: 497ef95c882dae58db33bf6b7d02c28a63909968f6a751a0ed9a29c97d12aa83
3
+ metadata.gz: 91782f59f8a8101bb7e6d431d0e5f6e095ae6c4beed07485534bbf11406a32e3
4
+ data.tar.gz: 4d336df923e86b2f2f820f073e8813779452512912122de398ee2fd19a8c9d8b
5
5
  SHA512:
6
- metadata.gz: 150b06a5b965394c1c7f00b95c7f3d1bb78e58855d300fb3041f90ad828fc023ca6e0cca02faad01274d58b3d5263bbf05acf337691ce5b89e98482596f30241
7
- data.tar.gz: 2524c76c7a7ea7101f3987f79e31270fe0e51091aabf1f8bc229b9854285757e01b4c335e09be348a5f850f7b2f5b1fe16f18d4ff0d72eda94496cc1a54a0b46
6
+ metadata.gz: cc5684ed82435f46ca36e99fa251073489ce870f15c9bb1484b43b4901049b1313fe6c5995fba020b0db717f0ebcac729ed09a68e889c25d4c148602da13a778
7
+ data.tar.gz: '08c7298e7cd72fff069fe3870b2f853816658ddafe9871b86c957eae8d5aa218e8893381db8fac530160c1cc7f920bcff1aabc67d4ab2bd2e9d77db6705ba8be'
data/README.md CHANGED
@@ -197,3 +197,16 @@ Betterment/NonStandardActions:
197
197
  - 'config/routes.rb'
198
198
  - 'config/other_routes.rb'
199
199
  ```
200
+
201
+ ### Betterment/HardcodedID
202
+
203
+ This cop identifies hardcoded IDs in your specs. You should not hardcode IDs in specs because the spec may flake due to an ID collision, leading to false positives, unique constraint violations (e.g. `PG::UniqueViolation`), and more.
204
+
205
+ Instead of hardcoding an ID, create the resource and reference its ID. If the ID refers to an identifier in an external system, consider using a [FactoryBot sequence](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#sequences).
206
+
207
+ This cop is capable of autocorrecting offenses, but it's not entirely safe. If you want to opt-in, add the following config to your `rubocop.yml` and run with `rubocop -A`:
208
+
209
+ ```yaml
210
+ Betterment/HardcodedID:
211
+ AutoCorrect: true
212
+ ```
data/config/default.yml CHANGED
@@ -67,6 +67,12 @@ Betterment/NonStandardActions:
67
67
  Include:
68
68
  - 'config/routes.rb'
69
69
 
70
+ Betterment/HardcodedID:
71
+ Description: 'Detects hardcoded IDs in specs'
72
+ StyleGuide: '#bettermenthardcodedid'
73
+ AutoCorrect: false
74
+ SafeAutoCorrect: false
75
+
70
76
  Layout/ParameterAlignment:
71
77
  Enabled: false
72
78
 
@@ -2,7 +2,7 @@ module RuboCop
2
2
  module Cop
3
3
  module Betterment
4
4
  class ActiveJobPerformable < Cop
5
- MSG = <<-DOC.freeze
5
+ MSG = <<~DOC.freeze
6
6
  Classes that are "performable" should be ActiveJobs
7
7
 
8
8
  class MyJob < ApplicationJob
@@ -23,16 +23,24 @@ module RuboCop
23
23
  PATTERN
24
24
 
25
25
  def on_class(node)
26
- return unless has_perform_method?(node)
27
26
  return if subclasses_application_job?(node)
27
+ return unless has_perform_method?(node)
28
28
 
29
- add_offense(node.children.first)
29
+ add_offense(node.identifier)
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def has_perform_method?(node)
35
- node.descendants.find { |n| is_perform_method?(n) }
35
+ possible_methods_within(node).any? { |n| is_perform_method?(n) }
36
+ end
37
+
38
+ def possible_methods_within(node)
39
+ if node.body.begin_type?
40
+ node.body.children
41
+ else
42
+ [node.body]
43
+ end
36
44
  end
37
45
  end
38
46
  end
@@ -0,0 +1,101 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class HardcodedID < Base
5
+ include RangeHelp
6
+ extend AutoCorrector
7
+
8
+ MSG = 'Hardcoded IDs cause flaky tests. Use a sequence instead.'.freeze
9
+
10
+ # @!method key(node)
11
+ def_node_matcher :key, '/^id$|_id$/'
12
+
13
+ # @!method value(node)
14
+ def_node_matcher :value, '{int | (str /^\d+$/)}'
15
+
16
+ # @!method pair(node, value_pattern)
17
+ def_node_matcher :pair, '(pair (sym #key) %1)'
18
+
19
+ # @!method hardcoded_id?(node)
20
+ def_node_matcher :hardcoded_id?, '#pair(#value)'
21
+
22
+ # @!method references_hardcoded_id?(node, method_name)
23
+ def_node_matcher :references_hardcoded_id?, '#pair((send nil? %1))'
24
+
25
+ # @!method on_factory(node)
26
+ def_node_matcher :on_factory, <<~PATTERN
27
+ (send (const nil? :FactoryBot) {:create | :create_list} _factory ... (hash $...))
28
+ PATTERN
29
+
30
+ # @!method on_let_id(node)
31
+ def_node_matcher :on_let_id, <<~PATTERN
32
+ (block (send nil? {:let | :let!} (sym $#key)) _block_args $#value)
33
+ PATTERN
34
+
35
+ def on_send(node)
36
+ each_factory_attribute(node) do |attribute_node|
37
+ next unless hardcoded_id?(attribute_node)
38
+
39
+ add_offense(attribute_node) do |corrector|
40
+ attribute = Utils::HardcodedAttribute.new(attribute_node)
41
+ correct_factory_usage(corrector, attribute) if attribute.correctable?
42
+ end
43
+ end
44
+ end
45
+
46
+ def on_block(node)
47
+ on_let_id(node) do |name, value|
48
+ node.parent&.each_descendant do |child|
49
+ each_factory_attribute(child) do |attribute_node|
50
+ next unless references_hardcoded_id?(attribute_node, name)
51
+
52
+ add_offense(node) do |corrector|
53
+ attribute = Utils::HardcodedAttribute.new(attribute_node)
54
+ corrector.replace(attribute.node.value, value.value.to_s) if attribute.correctable?
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ alias on_numblock on_block
61
+
62
+ private
63
+
64
+ def each_factory_attribute(node, &block)
65
+ on_factory(node) do |attributes|
66
+ attributes.each(&block)
67
+ end
68
+ end
69
+
70
+ def correct_factory_usage(corrector, attribute)
71
+ corrector.remove(with_comma(attribute.node))
72
+
73
+ attribute.each_integer_reference do |reference|
74
+ corrector.replace(reference, attribute.replacement)
75
+ end
76
+
77
+ attribute.each_string_reference do |reference|
78
+ attribute.each_range_within_string(reference) do |range|
79
+ corrector.replace(range, "\#{#{attribute.replacement}}")
80
+ end
81
+
82
+ ensure_double_quoted(corrector, reference)
83
+ end
84
+ end
85
+
86
+ def ensure_double_quoted(corrector, node)
87
+ if node.source.start_with?("'")
88
+ corrector.replace(node.loc.begin, '"')
89
+ corrector.replace(node.loc.end, '"')
90
+ end
91
+ end
92
+
93
+ def with_comma(attribute)
94
+ range = attribute.location.expression
95
+ range = range_with_surrounding_space(range)
96
+ range_with_surrounding_comma(range, :left)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,75 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ class HardcodedAttribute
5
+ extend RuboCop::NodePattern::Macros
6
+
7
+ attr_reader :node, :let_node, :let_name
8
+
9
+ def initialize(node)
10
+ @node = node
11
+ @let_node = node.parent&.parent&.parent
12
+ @let_name = extract_let_name(@let_node)
13
+ end
14
+
15
+ def correctable?
16
+ key == :id && !let_name.nil?
17
+ end
18
+
19
+ def replacement
20
+ "#{let_name}.#{key}"
21
+ end
22
+
23
+ def each_integer_reference
24
+ each_possible_reference(:int) do |ref|
25
+ yield ref if ref.value == value
26
+ end
27
+ end
28
+
29
+ def each_string_reference
30
+ each_possible_reference(:str) do |ref|
31
+ yield ref if ref.value.match?(value_pattern)
32
+ end
33
+ end
34
+
35
+ def each_range_within_string(reference)
36
+ reference.source.enum_for(:scan, value_pattern).each do
37
+ yield create_range(reference, Regexp.last_match)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def create_range(node, match)
44
+ range = node.loc.expression
45
+ begin_pos = range.begin_pos + match.begin(0)
46
+ end_pos = range.begin_pos + match.end(0)
47
+ range.with(begin_pos: begin_pos, end_pos: end_pos)
48
+ end
49
+
50
+ def key
51
+ node.key.value
52
+ end
53
+
54
+ def value
55
+ node.value.value.to_i
56
+ end
57
+
58
+ def value_pattern
59
+ /\b#{value}\b/
60
+ end
61
+
62
+ def each_possible_reference(type, &block)
63
+ let_node.parent.each_descendant(:block) do |block_node|
64
+ block_node.each_descendant(type, &block) unless block_node == let_node
65
+ end
66
+ end
67
+
68
+ # @!method extract_let_name(node)
69
+ def_node_matcher :extract_let_name, <<~PATTERN
70
+ (block (send nil? {:let | :let!} (sym $_let_name)) _block_args _block_body)
71
+ PATTERN
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,6 +1,7 @@
1
1
  require 'rubocop'
2
2
  require 'rubocop/cop/betterment/utils/parser'
3
3
  require 'rubocop/cop/betterment/utils/method_return_table'
4
+ require 'rubocop/cop/betterment/utils/hardcoded_attribute'
4
5
  require 'rubocop/cop/betterment/authorization_in_controller'
5
6
  require 'rubocop/cop/betterment/dynamic_params'
6
7
  require 'rubocop/cop/betterment/unscoped_find'
@@ -14,3 +15,4 @@ require 'rubocop/cop/betterment/implicit_redirect_type'
14
15
  require 'rubocop/cop/betterment/active_job_performable'
15
16
  require 'rubocop/cop/betterment/allowlist_blocklist'
16
17
  require 'rubocop/cop/betterment/server_error_assertion'
18
+ require 'rubocop/cop/betterment/hardcoded_id'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: betterlint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Development
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-17 00:00:00.000000000 Z
11
+ date: 2022-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -137,6 +137,7 @@ files:
137
137
  - lib/rubocop/cop/betterment/allowlist_blocklist.rb
138
138
  - lib/rubocop/cop/betterment/authorization_in_controller.rb
139
139
  - lib/rubocop/cop/betterment/dynamic_params.rb
140
+ - lib/rubocop/cop/betterment/hardcoded_id.rb
140
141
  - lib/rubocop/cop/betterment/implicit_redirect_type.rb
141
142
  - lib/rubocop/cop/betterment/memoization_with_arguments.rb
142
143
  - lib/rubocop/cop/betterment/non_standard_actions.rb
@@ -146,6 +147,7 @@ files:
146
147
  - lib/rubocop/cop/betterment/timeout.rb
147
148
  - lib/rubocop/cop/betterment/unsafe_job.rb
148
149
  - lib/rubocop/cop/betterment/unscoped_find.rb
150
+ - lib/rubocop/cop/betterment/utils/hardcoded_attribute.rb
149
151
  - lib/rubocop/cop/betterment/utils/method_return_table.rb
150
152
  - lib/rubocop/cop/betterment/utils/parser.rb
151
153
  homepage: