rubocop-dev_doc 0.1.0 → 0.2.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.
@@ -0,0 +1,91 @@
1
+ module RuboCop
2
+ module Cop
3
+ module DevDoc
4
+ module Style
5
+ # Flag direct `==` / `!=` comparisons between a known-string source
6
+ # and a symbol literal. Such comparisons are silently always false
7
+ # (`'draft' == :draft` is `false` in Ruby), so they signal that the
8
+ # caller forgot to convert at the boundary.
9
+ #
10
+ # ## Rationale
11
+ # Strings and symbols are not equal in Ruby, and string-typed values
12
+ # arrive from boundaries the developer doesn't control: HTTP params,
13
+ # request headers, ENV. Comparing one of these directly against a
14
+ # symbol literal is a guaranteed bug. Convert at the boundary instead
15
+ # — see `best_practices/backend/en/01a_defensive_programming.md` item 7.
16
+ #
17
+ # ❌
18
+ # if params[:status] == :draft # always false
19
+ #
20
+ # ✔ Convert at the boundary, then compare
21
+ # @filter_status = params[:status]&.to_sym
22
+ # if @filter_status == :draft
23
+ #
24
+ # ## Sources detected
25
+ # - `params[…]`
26
+ # - `request.headers[…]`
27
+ # - `ENV[…]`
28
+ #
29
+ # ## Limitations
30
+ # The cop only catches the *direct* comparison form. Once the value
31
+ # flows into a local or instance variable, type information is lost
32
+ # and Rubocop cannot trace it back to the original string source.
33
+ # Those cases are caught by review.
34
+ #
35
+ # @example
36
+ # # bad
37
+ # params[:status] == :draft
38
+ # :production == ENV['MODE']
39
+ # request.headers['X-Mode'] != :debug
40
+ #
41
+ # # good
42
+ # params[:status]&.to_sym == :draft
43
+ # ENV['MODE'] == 'production'
44
+ class StringSymbolComparison < Base
45
+ MSG = 'Comparing string-typed `%<source>s` to a symbol literal is always false. ' \
46
+ 'Convert at the boundary with `&.to_sym` or compare against a string instead ' \
47
+ '(see backend/01a_defensive_programming.md item 7).'.freeze
48
+
49
+ RESTRICT_ON_SEND = %i[== !=].freeze
50
+
51
+ def_node_matcher :params_access?, <<~PATTERN
52
+ (send (send nil? :params) :[] _)
53
+ PATTERN
54
+
55
+ def_node_matcher :request_headers_access?, <<~PATTERN
56
+ (send (send _ :headers) :[] _)
57
+ PATTERN
58
+
59
+ def_node_matcher :env_access?, <<~PATTERN
60
+ (send (const nil? :ENV) :[] _)
61
+ PATTERN
62
+
63
+ def on_send(node)
64
+ lhs = node.receiver
65
+ rhs = node.first_argument
66
+ return unless lhs && rhs
67
+
68
+ source_node = pick_string_source(lhs, rhs)
69
+ return unless source_node
70
+
71
+ add_offense(node, message: format(MSG, source: source_node.source))
72
+ end
73
+
74
+ private
75
+
76
+ def pick_string_source(left, right)
77
+ if string_source?(left) && right.sym_type?
78
+ left
79
+ elsif string_source?(right) && left.sym_type?
80
+ right
81
+ end
82
+ end
83
+
84
+ def string_source?(node)
85
+ params_access?(node) || request_headers_access?(node) || env_access?(node)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,3 +1,4 @@
1
1
  require "rubocop"
2
+ require "rubocop-rails"
2
3
  require "lint_roller"
3
4
  require_relative "rubocop/dev_doc"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-dev_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dev-doc contributors
@@ -10,65 +10,73 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rubocop
13
+ name: lint_roller
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.72'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.72'
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: rubocop-rails
27
+ name: rubocop
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '2.0'
32
+ version: '1.72'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '2.0'
39
+ version: '1.72'
40
40
  - !ruby/object:Gem::Dependency
41
- name: lint_roller
41
+ name: rubocop-rails
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '2.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '2.0'
54
54
  executables: []
55
55
  extensions: []
56
56
  extra_rdoc_files: []
57
57
  files:
58
58
  - config/default.yml
59
59
  - lib/rubocop-dev_doc.rb
60
+ - lib/rubocop/cop/dev_doc/migration/amount_column_in_cents.rb
61
+ - lib/rubocop/cop/dev_doc/migration/avoid_bypassing_validation.rb
60
62
  - lib/rubocop/cop/dev_doc/migration/avoid_column_default.rb
61
63
  - lib/rubocop/cop/dev_doc/migration/avoid_json_column.rb
62
- - lib/rubocop/cop/dev_doc/migration/avoid_update_column.rb
63
64
  - lib/rubocop/cop/dev_doc/migration/avoid_vague_column_names.rb
64
65
  - lib/rubocop/cop/dev_doc/migration/date_column_naming.rb
66
+ - lib/rubocop/cop/dev_doc/migration/no_create_join_table.rb
65
67
  - lib/rubocop/cop/dev_doc/migration/prefer_belongs_to.rb
68
+ - lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
66
69
  - lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
70
+ - lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
71
+ - lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
72
+ - lib/rubocop/cop/dev_doc/rails/enum_must_be_symbolized.rb
67
73
  - lib/rubocop/cop/dev_doc/rails/no_deliver_later_in_transaction.rb
68
74
  - lib/rubocop/cop/dev_doc/rails/no_perform_later_in_model.rb
69
75
  - lib/rubocop/cop/dev_doc/route/resources_require_only.rb
70
76
  - lib/rubocop/cop/dev_doc/style/avoid_head_response.rb
77
+ - lib/rubocop/cop/dev_doc/style/avoid_options_hash.rb
71
78
  - lib/rubocop/cop/dev_doc/style/avoid_send.rb
79
+ - lib/rubocop/cop/dev_doc/style/string_symbol_comparison.rb
72
80
  - lib/rubocop/dev_doc.rb
73
81
  - lib/rubocop/dev_doc/plugin.rb
74
82
  - lib/rubocop/dev_doc/version.rb
@@ -1,53 +0,0 @@
1
- module RuboCop
2
- module Cop
3
- module DevDoc
4
- module Migration
5
- # Avoid `update_column`, `update_columns`, and `update_all` in migrations.
6
- #
7
- # ## Rationale
8
- # Avoid bypassing validation unless absolutely necessary. These methods
9
- # skip validations and callbacks, which hides data integrity issues
10
- # rather than surfacing them.
11
- #
12
- # Even in migrations, check the code to see if there is any blatant
13
- # reason why existing records may be invalid. If there is, fix those
14
- # records first rather than bypassing validation.
15
- #
16
- # ❌ Bypasses validation — hides data integrity issues
17
- # Faq.where(purpose: nil).update_all(purpose: :intro)
18
- #
19
- # ✔️ Runs validation — surfaces problems early
20
- # Faq.where(purpose: nil).find_each do |faq|
21
- # faq.purpose = :intro
22
- # faq.save!
23
- # end
24
- #
25
- # NOTE: The broader principle ("avoid bypassing validation") also covers
26
- # things this cop does not catch, e.g. `save(validate: false)`,
27
- # `insert_all`, `upsert_all`, `delete_all`. Apply the same judgement to
28
- # those patterns.
29
- #
30
- # @example
31
- # # bad
32
- # Faq.where(purpose: nil).update_all(purpose: :intro)
33
- #
34
- # # bad
35
- # user.update_column(:status, 'active')
36
- #
37
- # # good
38
- # Faq.where(purpose: nil).find_each do |faq|
39
- # faq.purpose = :intro
40
- # faq.save!
41
- # end
42
- class AvoidUpdateColumn < Base
43
- MSG = 'Avoid `%<method>s` in migrations; it bypasses validations. Use `save!` instead.'.freeze
44
- RESTRICT_ON_SEND = %i[update_column update_columns update_all].freeze
45
-
46
- def on_send(node)
47
- add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
48
- end
49
- end
50
- end
51
- end
52
- end
53
- end