rubocop-yiffspace 0.0.1
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 +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/jsonSchemas.xml +25 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/rubocop-plugin.iml +65 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +33 -0
- data/config/custom.yml +17 -0
- data/config/default.yml +54 -0
- data/config/rails.yml +8 -0
- data/config/shared/bundler.yml +1 -0
- data/config/shared/gemspec.yml +1 -0
- data/config/shared/layout.yml +18 -0
- data/config/shared/lint.yml +4 -0
- data/config/shared/metrics.yml +35 -0
- data/config/shared/migration.yml +1 -0
- data/config/shared/naming.yml +7 -0
- data/config/shared/rails.yml +14 -0
- data/config/shared/security.yml +1 -0
- data/config/shared/style.yml +45 -0
- data/config/shared.yml +12 -0
- data/lib/rubocop/cop/mixin/active_record_helper.rb +18 -0
- data/lib/rubocop/cop/mixin/node_formatting_helper.rb +59 -0
- data/lib/rubocop/cop/yiff_space/belongs_to_user.rb +121 -0
- data/lib/rubocop/cop/yiff_space/belongs_to_user_invalid_ip.rb +107 -0
- data/lib/rubocop/cop/yiff_space/belongs_to_user_missing_ip.rb +105 -0
- data/lib/rubocop/cop/yiff_space/current_user_outside_of_requests.rb +194 -0
- data/lib/rubocop/cop/yiff_space/resolvable_user.rb +113 -0
- data/lib/rubocop/cop/yiff_space/user_to_id.rb +64 -0
- data/lib/rubocop/cop/yiffspace_cops.rb +10 -0
- data/lib/rubocop/yiff_space/plugin.rb +31 -0
- data/lib/rubocop/yiff_space/version.rb +7 -0
- data/lib/rubocop/yiff_space.rb +8 -0
- data/lib/rubocop-yiffspace.rb +9 -0
- data/spec/rubocop/cop/yiff_space/belongs_to_user_invalid_ip_spec.rb +109 -0
- data/spec/rubocop/cop/yiff_space/belongs_to_user_missing_ip_spec.rb +91 -0
- data/spec/rubocop/cop/yiff_space/belongs_to_user_spec.rb +158 -0
- data/spec/rubocop/cop/yiff_space/current_user_outside_of_requests_spec.rb +398 -0
- data/spec/rubocop/cop/yiff_space/resolvable_user_spec.rb +99 -0
- data/spec/rubocop/cop/yiff_space/user_to_id_spec.rb +67 -0
- data/spec/spec_helper.rb +20 -0
- metadata +118 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Enforces using `belongs_to_user` instead of `belongs_to` for
|
|
7
|
+
# User associations. `belongs_to_user` provides additional helper methods
|
|
8
|
+
# and is the project standard for any relation backed by a User record.
|
|
9
|
+
#
|
|
10
|
+
# It matches both `belongs_to(:user)` and associations with an explicit
|
|
11
|
+
# `class_name: "User"` option, auto-correcting both forms.
|
|
12
|
+
#
|
|
13
|
+
# The class name is configurable via `ClassName` (default: `User`).
|
|
14
|
+
# The inferred attribute name is the snake_case of the last segment
|
|
15
|
+
# (e.g. `ClassName: CurrentUser` matches `belongs_to(:current_user)`).
|
|
16
|
+
#
|
|
17
|
+
# @safety
|
|
18
|
+
# `belongs_to_user` must be defined in the target codebase, and the
|
|
19
|
+
# backing column must reference a User. Auto-correct is unsafe when
|
|
20
|
+
# either condition cannot be verified statically.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # bad
|
|
24
|
+
# belongs_to(:user)
|
|
25
|
+
#
|
|
26
|
+
# # bad
|
|
27
|
+
# belongs_to(:creator, class_name: "User")
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# belongs_to_user(:user)
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# belongs_to_user(:creator)
|
|
34
|
+
#
|
|
35
|
+
# @example ClassName: CurrentUser
|
|
36
|
+
# # bad
|
|
37
|
+
# belongs_to(:current_user, optional: false)
|
|
38
|
+
#
|
|
39
|
+
# # bad
|
|
40
|
+
# belongs_to(:creator, class_name: "CurrentUser")
|
|
41
|
+
#
|
|
42
|
+
# # good
|
|
43
|
+
# belongs_to_user(:current_user, optional: false)
|
|
44
|
+
#
|
|
45
|
+
# # good
|
|
46
|
+
# belongs_to_user(:creator)
|
|
47
|
+
#
|
|
48
|
+
class BelongsToUser < Base
|
|
49
|
+
extend(AutoCorrector)
|
|
50
|
+
include(NodeFormattingHelper)
|
|
51
|
+
|
|
52
|
+
MSG = "Use `belongs_to_user(%<attr>s)` instead of `belongs_to(%<attr>s)`"
|
|
53
|
+
|
|
54
|
+
# requires_gem("activerecord")
|
|
55
|
+
|
|
56
|
+
# @!method belongs_to_user?(node)
|
|
57
|
+
def_node_matcher(:belongs_to_user?, <<~PATTERN)
|
|
58
|
+
(send nil? :belongs_to $_ $...)
|
|
59
|
+
PATTERN
|
|
60
|
+
|
|
61
|
+
def on_send(node)
|
|
62
|
+
belongs_to_user?(node) do |receiver, code|
|
|
63
|
+
return unless receiver.type?(:str, :sym)
|
|
64
|
+
|
|
65
|
+
attr = format_node(receiver)
|
|
66
|
+
|
|
67
|
+
return if attr.nil? || attr.empty? || !code.last&.hash_type?
|
|
68
|
+
|
|
69
|
+
options = format_node(code.last, {})
|
|
70
|
+
|
|
71
|
+
# Match belongs_to(:user) (or configured attr name)
|
|
72
|
+
if attr.to_sym == user_attr_name && !options.key?(:class_name)
|
|
73
|
+
register_offense(node, attr, options)
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Match belongs_to(attr, class_name: "User") (or configured ClassName)
|
|
78
|
+
register_offense(node, attr, options) if options[:class_name] == user_class_name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def on_csend(node)
|
|
83
|
+
on_send(node)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def register_offense(node, attr, options)
|
|
89
|
+
message = format(MSG, attr: attr.inspect)
|
|
90
|
+
|
|
91
|
+
add_offense(node, message: message) do |corrector|
|
|
92
|
+
new_options = format_new_options(attr, options)
|
|
93
|
+
|
|
94
|
+
corrector.replace(node, "belongs_to_user(#{new_options})")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def user_class_name
|
|
99
|
+
cop_config.fetch("ClassName", "User")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def user_attr_name
|
|
103
|
+
last_segment = user_class_name.split("::").last
|
|
104
|
+
last_segment.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_new_options(attr, options)
|
|
108
|
+
list = [attr.inspect]
|
|
109
|
+
|
|
110
|
+
options.delete(:class_name)
|
|
111
|
+
if options.any?
|
|
112
|
+
options.each do |k, v|
|
|
113
|
+
list << "#{k}: #{v.inspect}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
list.join(", ")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Detects when `ip:` is specified in a `belongs_to_user` call
|
|
7
|
+
# but the corresponding IP address column does not exist in the database
|
|
8
|
+
# schema. It auto-corrects by removing the invalid `ip:` option.
|
|
9
|
+
#
|
|
10
|
+
# @safety
|
|
11
|
+
# This cop reads the database schema to determine whether the column
|
|
12
|
+
# exists. It will not flag anything if the schema cannot be loaded.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad - no `creator_ip_addr` column exists in the schema
|
|
16
|
+
# belongs_to_user(:creator, ip: true)
|
|
17
|
+
#
|
|
18
|
+
# # bad - no `creator_custom` column exists in the schema
|
|
19
|
+
# belongs_to_user(:creator, ip: "creator_custom")
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# belongs_to_user(:creator)
|
|
23
|
+
#
|
|
24
|
+
# # good - `creator_ip_addr` column exists in the schema
|
|
25
|
+
# belongs_to_user(:creator, ip: true)
|
|
26
|
+
#
|
|
27
|
+
class BelongsToUserInvalidIp < Base
|
|
28
|
+
extend(AutoCorrector)
|
|
29
|
+
include(ActiveRecordHelper)
|
|
30
|
+
include(NodeFormattingHelper)
|
|
31
|
+
|
|
32
|
+
MSG = "`ip: %<ip_set>s` set for `belongs_to_user(%<attr>s)` when `%<ip_attr>s` column does not exist"
|
|
33
|
+
|
|
34
|
+
# requires_gem("activerecord")
|
|
35
|
+
|
|
36
|
+
# @!method belongs_to_user?(node)
|
|
37
|
+
def_node_matcher(:belongs_to_user?, <<~PATTERN)
|
|
38
|
+
(send nil? :belongs_to_user $_ $...)
|
|
39
|
+
PATTERN
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
belongs_to_user?(node) do |receiver, code|
|
|
43
|
+
return unless receiver.type?(:str, :sym)
|
|
44
|
+
|
|
45
|
+
attr = format_node(receiver)
|
|
46
|
+
return if attr.nil? || attr.empty?
|
|
47
|
+
|
|
48
|
+
options = format_node(code.last, {})
|
|
49
|
+
return if [nil, "", false].include?(options[:ip])
|
|
50
|
+
|
|
51
|
+
return unless schema
|
|
52
|
+
|
|
53
|
+
table = table(node)
|
|
54
|
+
return unless table
|
|
55
|
+
|
|
56
|
+
column = options[:ip] == true ? "#{attr}_ip_addr" : options[:ip].to_s
|
|
57
|
+
exists = table.with_column?(name: column)
|
|
58
|
+
|
|
59
|
+
return if exists
|
|
60
|
+
|
|
61
|
+
register_offense(node, attr, options, column, options[:ip])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_csend(node)
|
|
66
|
+
on_send(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def class_node(node)
|
|
72
|
+
node.each_ancestor.find(&:class_type?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def table(node)
|
|
76
|
+
klass = class_node(node)
|
|
77
|
+
return unless klass
|
|
78
|
+
|
|
79
|
+
schema.table_by(name: table_name(klass))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def register_offense(node, attr, options, column, original)
|
|
83
|
+
message = format(MSG, attr: attr.inspect, ip_attr: "#{table(node).name}.#{column}".inspect,
|
|
84
|
+
ip_set: original.inspect)
|
|
85
|
+
|
|
86
|
+
add_offense(node, message: message) do |corrector|
|
|
87
|
+
new_options = format_new_options(attr, options)
|
|
88
|
+
|
|
89
|
+
corrector.replace(node, "belongs_to_user(#{new_options})")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def format_new_options(attr, options)
|
|
94
|
+
list = [attr.inspect]
|
|
95
|
+
|
|
96
|
+
options.delete(:ip)
|
|
97
|
+
if options.any?
|
|
98
|
+
options.each do |k, v|
|
|
99
|
+
list << "#{k}: #{v.inspect}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
list.join(", ")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Detects when a `belongs_to_user` call is missing `ip: true`
|
|
7
|
+
# even though a corresponding `<attr>_ip_addr` column exists in the
|
|
8
|
+
# database schema. It auto-corrects by adding `ip: true`.
|
|
9
|
+
#
|
|
10
|
+
# @safety
|
|
11
|
+
# This cop reads the database schema to determine whether the column
|
|
12
|
+
# exists. It will not flag anything if the schema cannot be loaded.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad - `creator_ip_addr` column exists but `ip:` is not specified
|
|
16
|
+
# belongs_to_user(:creator)
|
|
17
|
+
#
|
|
18
|
+
# # bad - `ip: false` explicitly opts out but the column exists
|
|
19
|
+
# belongs_to_user(:creator, ip: false)
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# belongs_to_user(:creator, ip: true)
|
|
23
|
+
#
|
|
24
|
+
# # good - no `creator_ip_addr` column exists in the schema
|
|
25
|
+
# belongs_to_user(:creator)
|
|
26
|
+
#
|
|
27
|
+
class BelongsToUserMissingIp < Base
|
|
28
|
+
extend(AutoCorrector)
|
|
29
|
+
include(ActiveRecordHelper)
|
|
30
|
+
include(NodeFormattingHelper)
|
|
31
|
+
|
|
32
|
+
MSG = "Specify `ip: true` when a `belongs_to_user(%<attr>s)` relation has a corresponding `%<ip_attr>s` column"
|
|
33
|
+
|
|
34
|
+
# requires_gem("activerecord")
|
|
35
|
+
|
|
36
|
+
# @!method belongs_to_user?(node)
|
|
37
|
+
def_node_matcher(:belongs_to_user?, <<~PATTERN)
|
|
38
|
+
(send nil? :belongs_to_user $_ $...)
|
|
39
|
+
PATTERN
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
belongs_to_user?(node) do |receiver, code|
|
|
43
|
+
return unless receiver.type?(:str, :sym)
|
|
44
|
+
|
|
45
|
+
attr = format_node(receiver)
|
|
46
|
+
return if attr.nil? || attr.empty?
|
|
47
|
+
|
|
48
|
+
options = format_node(code.last, {})
|
|
49
|
+
return unless [nil, "", false].include?(options[:ip])
|
|
50
|
+
|
|
51
|
+
return unless schema
|
|
52
|
+
|
|
53
|
+
table = table(node)
|
|
54
|
+
return unless table
|
|
55
|
+
|
|
56
|
+
column = "#{attr}_ip_addr"
|
|
57
|
+
exists = table.with_column?(name: column)
|
|
58
|
+
|
|
59
|
+
return unless exists
|
|
60
|
+
|
|
61
|
+
register_offense(node, attr, options, column)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_csend(node)
|
|
66
|
+
on_send(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def class_node(node)
|
|
72
|
+
node.each_ancestor.find(&:class_type?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def table(node)
|
|
76
|
+
klass = class_node(node)
|
|
77
|
+
return unless klass
|
|
78
|
+
|
|
79
|
+
schema.table_by(name: table_name(klass))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def register_offense(node, attr, options, column)
|
|
83
|
+
message = format(MSG, attr: attr.inspect, ip_attr: column.inspect)
|
|
84
|
+
|
|
85
|
+
add_offense(node, message: message) do |corrector|
|
|
86
|
+
new_options = format_new_options(attr, options)
|
|
87
|
+
|
|
88
|
+
corrector.replace(node, "belongs_to_user(#{new_options})")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_new_options(attr, options)
|
|
93
|
+
list = [attr.inspect, "ip: true"]
|
|
94
|
+
|
|
95
|
+
if options.any?
|
|
96
|
+
options.each do |k, v|
|
|
97
|
+
list << "#{k}: #{v.inspect}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
list.join(", ")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Prevents use of the CurrentUser class outside of the request
|
|
7
|
+
# cycle. CurrentUser relies on thread-local state set by request
|
|
8
|
+
# middleware and is unavailable in background jobs, rake tasks, or other
|
|
9
|
+
# contexts outside of the request lifecycle.
|
|
10
|
+
#
|
|
11
|
+
# The class name is configurable via `ClassName` (default: `CurrentUser`).
|
|
12
|
+
# References inside the class that defines CurrentUser itself are always
|
|
13
|
+
# ignored. Controllers, views, helpers, presenters, and decorators are
|
|
14
|
+
# excluded by default. Use `IgnoredMethods`, `IgnoredMethodPrefixes`,
|
|
15
|
+
# `IgnoredMethodSuffixes`, or `IgnoredMethodPatterns` to allow
|
|
16
|
+
# CurrentUser in specific methods in checked files.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# # bad
|
|
20
|
+
# def process_records
|
|
21
|
+
# CurrentUser.id
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # bad
|
|
25
|
+
# def notify
|
|
26
|
+
# CurrentUser.email
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good - inside the CurrentUser class definition itself
|
|
30
|
+
# class CurrentUser
|
|
31
|
+
# def self.id
|
|
32
|
+
# RequestStore[:current_user]&.id
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# # good (IgnoredMethodSuffixes: [_current])
|
|
37
|
+
# def user_current
|
|
38
|
+
# CurrentUser.id
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # good (IgnoredMethodPrefixes: [apionly_])
|
|
42
|
+
# def apionly_serialize
|
|
43
|
+
# CurrentUser.email
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# # good (IgnoredMethodPatterns: [/_current_user_/])
|
|
47
|
+
# def serialize_current_user_email
|
|
48
|
+
# CurrentUser.email
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
class CurrentUserOutsideOfRequests < Base
|
|
52
|
+
MSG = "`%<class_name>s` should only be used within the request cycle (controllers, views, helpers, decorators)"
|
|
53
|
+
|
|
54
|
+
def on_send(node)
|
|
55
|
+
return unless starts_with_current_user?(node)
|
|
56
|
+
return if ignored_method?(node)
|
|
57
|
+
return if inside_current_user_class?(node)
|
|
58
|
+
|
|
59
|
+
add_offense(node, message: message)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_csend(node)
|
|
63
|
+
on_send(node)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_const(node)
|
|
67
|
+
return unless current_user?(node)
|
|
68
|
+
return if ignored_method?(node)
|
|
69
|
+
return if inside_current_user_class?(node)
|
|
70
|
+
return if node.parent&.send_type? && node.parent.receiver == node
|
|
71
|
+
|
|
72
|
+
add_offense(node, message: message)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def message
|
|
78
|
+
format(MSG, class_name: current_user_class_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def current_user_class_name
|
|
82
|
+
cop_config.fetch("ClassName", "CurrentUser")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def current_user?(node)
|
|
86
|
+
return false unless node.const_type?
|
|
87
|
+
|
|
88
|
+
class_parts = current_user_class_name.split("::")
|
|
89
|
+
is_absolute, ref_parts = extract_const_info(node)
|
|
90
|
+
|
|
91
|
+
return ref_parts == class_parts if is_absolute
|
|
92
|
+
|
|
93
|
+
return false if ref_parts.length > class_parts.length
|
|
94
|
+
return false unless class_parts.last(ref_parts.length) == ref_parts
|
|
95
|
+
|
|
96
|
+
expected_ns = class_parts.first(class_parts.length - ref_parts.length)
|
|
97
|
+
|
|
98
|
+
ancestors = node.each_ancestor(:module, :class).to_a.reverse
|
|
99
|
+
cumulative = []
|
|
100
|
+
idx = 0
|
|
101
|
+
inner_ancestors = []
|
|
102
|
+
|
|
103
|
+
ancestors.each do |anc|
|
|
104
|
+
parts = const_parts(anc.identifier)
|
|
105
|
+
inner_ancestors << anc if idx + parts.length > expected_ns.length
|
|
106
|
+
cumulative.concat(parts)
|
|
107
|
+
idx += parts.length
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return false unless cumulative.first(expected_ns.length) == expected_ns
|
|
111
|
+
|
|
112
|
+
inner_ancestors.none? { |anc| defines_const_in_body?(anc, ref_parts[0]) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def starts_with_current_user?(node)
|
|
116
|
+
node.receiver && current_user?(node.receiver)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def inside_current_user_class?(node)
|
|
120
|
+
node.each_ancestor(:class).any? do |class_node|
|
|
121
|
+
resolved_class_name(class_node) == current_user_class_name
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def resolved_class_name(class_node)
|
|
126
|
+
parts = const_parts(class_node.identifier)
|
|
127
|
+
class_node.each_ancestor(:module, :class) { |ancestor| parts = const_parts(ancestor.identifier) + parts }
|
|
128
|
+
parts.join("::")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def const_parts(const_node)
|
|
132
|
+
parts = []
|
|
133
|
+
node = const_node
|
|
134
|
+
while node&.const_type?
|
|
135
|
+
parts.unshift(node.short_name.to_s)
|
|
136
|
+
node = node.namespace
|
|
137
|
+
end
|
|
138
|
+
parts
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def extract_const_info(node)
|
|
142
|
+
parts = []
|
|
143
|
+
current = node
|
|
144
|
+
while current&.const_type?
|
|
145
|
+
parts.unshift(current.short_name.to_s)
|
|
146
|
+
current = current.namespace
|
|
147
|
+
end
|
|
148
|
+
[current&.cbase_type? || false, parts]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def defines_const_in_body?(scope_node, name)
|
|
152
|
+
scope_node.body&.each_child_node(:class, :module)&.any? do |child|
|
|
153
|
+
child.identifier.short_name.to_s == name
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ignored_method?(node)
|
|
158
|
+
enclosing_method = node.each_ancestor(:any_def).first
|
|
159
|
+
|
|
160
|
+
return false unless enclosing_method&.any_def_type?
|
|
161
|
+
|
|
162
|
+
method_name = enclosing_method.method_name.to_s
|
|
163
|
+
ignored.any? { |pattern| pattern.match?(method_name) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def ignored
|
|
167
|
+
@ignored ||= begin
|
|
168
|
+
pattern = ignored_patterns
|
|
169
|
+
prefix = ignored_prefixes.map { |p| Regexp.new("^#{p}") }
|
|
170
|
+
suffix = ignored_suffixes.map { |p| Regexp.new("#{p}$") }
|
|
171
|
+
methods = ignored_methods.map { |p| Regexp.new("^#{p}$") }
|
|
172
|
+
[*pattern, *prefix, *suffix, *methods]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def ignored_patterns
|
|
177
|
+
@ignored_patterns ||= Array(cop_config["IgnoredMethodPatterns"]).map { |p| Regexp.new(p) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def ignored_prefixes
|
|
181
|
+
@ignored_prefixes ||= Array(cop_config["IgnoredMethodPrefixes"])
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def ignored_suffixes
|
|
185
|
+
@ignored_suffixes ||= Array(cop_config["IgnoredMethodSuffixes"])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def ignored_methods
|
|
189
|
+
@ignored_methods ||= Array(cop_config["IgnoredMethods"])
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Detects `resolvable` calls that should instead use
|
|
7
|
+
# `belongs_to_user` because the corresponding `<attr>_id` column exists
|
|
8
|
+
# in the database schema. It auto-corrects to `belongs_to_user`, adding
|
|
9
|
+
# `ip: true` when a `<attr>_ip_addr` column also exists.
|
|
10
|
+
#
|
|
11
|
+
# @safety
|
|
12
|
+
# This cop reads the database schema to determine whether the columns
|
|
13
|
+
# exist. It will not flag anything if the schema cannot be loaded.
|
|
14
|
+
# `belongs_to_user` must be defined in the target codebase.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad - `creator_id` column exists in the schema
|
|
18
|
+
# resolvable(:creator)
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# belongs_to_user(:creator)
|
|
22
|
+
#
|
|
23
|
+
# # bad - `creator_id` and `creator_ip_addr` columns exist in the schema
|
|
24
|
+
# resolvable(:creator)
|
|
25
|
+
#
|
|
26
|
+
# # good
|
|
27
|
+
# belongs_to_user(:creator, ip: true)
|
|
28
|
+
#
|
|
29
|
+
class ResolvableUser < Base
|
|
30
|
+
extend(AutoCorrector)
|
|
31
|
+
include(ActiveRecordHelper)
|
|
32
|
+
include(NodeFormattingHelper)
|
|
33
|
+
|
|
34
|
+
MSG = "use `belongs_to_user(%<attr>s)` when `%<id_column>s` column exists"
|
|
35
|
+
MSG_IP = "use `belongs_to_user(%<attr>s, ip: true)` when `%<id_column>s` and `%<ip_column>s` columns exist"
|
|
36
|
+
|
|
37
|
+
# @!method resolvable?(node)
|
|
38
|
+
def_node_matcher(:resolvable?, <<~PATTERN)
|
|
39
|
+
(send nil? :resolvable $_ $...)
|
|
40
|
+
PATTERN
|
|
41
|
+
|
|
42
|
+
def on_send(node)
|
|
43
|
+
resolvable?(node) do |receiver, code|
|
|
44
|
+
return unless receiver.type?(:str, :sym)
|
|
45
|
+
|
|
46
|
+
attr = format_node(receiver)
|
|
47
|
+
return if attr.nil? || attr.empty?
|
|
48
|
+
|
|
49
|
+
options = format_node(code.last, {})
|
|
50
|
+
|
|
51
|
+
return unless schema
|
|
52
|
+
|
|
53
|
+
table = table(node)
|
|
54
|
+
return unless table
|
|
55
|
+
|
|
56
|
+
column = "#{attr}_id"
|
|
57
|
+
ip_column = "#{attr}_ip_addr"
|
|
58
|
+
exists = table.with_column?(name: column)
|
|
59
|
+
ip_exists = table.with_column?(name: ip_column)
|
|
60
|
+
|
|
61
|
+
return unless exists
|
|
62
|
+
|
|
63
|
+
options[:ip] = true if ip_exists
|
|
64
|
+
|
|
65
|
+
register_offense(node, attr, options, column, ip_column)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_csend(node)
|
|
70
|
+
on_send(node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def class_node(node)
|
|
76
|
+
node.each_ancestor.find(&:class_type?)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def table(node)
|
|
80
|
+
klass = class_node(node)
|
|
81
|
+
return unless klass
|
|
82
|
+
|
|
83
|
+
schema.table_by(name: table_name(klass))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def register_offense(node, attr, options, id_column, ip_column)
|
|
87
|
+
message = if options[:ip]
|
|
88
|
+
format(MSG_IP, attr: attr.inspect, id_column: id_column.inspect, ip_column: ip_column.inspect)
|
|
89
|
+
else
|
|
90
|
+
format(MSG, attr: attr.inspect, id_column: id_column.inspect)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
add_offense(node, message: message) do |corrector|
|
|
94
|
+
new_options = format_new_options(attr, options)
|
|
95
|
+
|
|
96
|
+
corrector.replace(node, "belongs_to_user(#{new_options})")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def format_new_options(attr, options)
|
|
101
|
+
list = [attr.inspect]
|
|
102
|
+
|
|
103
|
+
if options.any?
|
|
104
|
+
options.slice(:ip, :clone).merge(options.except(:ip, :clone)).each do |k, v|
|
|
105
|
+
list << "#{k}: #{v.inspect}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
list.join(", ")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module YiffSpace
|
|
6
|
+
# Detects the ternary pattern `x.is_a?(User) ? x.id : x` and
|
|
7
|
+
# enforces use of the `u2id` helper instead, which handles both User
|
|
8
|
+
# objects and raw IDs without the inline type check.
|
|
9
|
+
#
|
|
10
|
+
# @safety
|
|
11
|
+
# `u2id` must be defined in the target codebase. The auto-correct is
|
|
12
|
+
# safe when `u2id` accepts both User objects and integer IDs.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# user.is_a?(User) ? user.id : user
|
|
17
|
+
#
|
|
18
|
+
# # bad
|
|
19
|
+
# creator.is_a?(User) ? creator.id : creator
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# u2id(user)
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# u2id(creator)
|
|
26
|
+
#
|
|
27
|
+
class UserToId < Base
|
|
28
|
+
extend(AutoCorrector)
|
|
29
|
+
include(NodeFormattingHelper)
|
|
30
|
+
|
|
31
|
+
MSG = "Use `u2id(%<var>s)` instead of `%<original>s`"
|
|
32
|
+
|
|
33
|
+
def on_if(node)
|
|
34
|
+
return unless node.ternary?
|
|
35
|
+
|
|
36
|
+
return unless user_check?(node.condition)
|
|
37
|
+
return unless id_call?(node.if_branch)
|
|
38
|
+
return unless same_value?(node.if_branch.receiver, node.else_branch)
|
|
39
|
+
|
|
40
|
+
message = format(MSG, var: node.else_branch.source, original: node.source)
|
|
41
|
+
add_offense(node, message: message) do |corrector|
|
|
42
|
+
corrector.replace(node, "u2id(#{node.else_branch.source})")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def user_check?(node)
|
|
49
|
+
node&.send_type? &&
|
|
50
|
+
node.method?(:is_a?) &&
|
|
51
|
+
node.first_argument&.const_name == "User"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def id_call?(node)
|
|
55
|
+
node&.send_type? && node.method?(:id)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def same_value?(a, b)
|
|
59
|
+
a && b && a.source == b.source
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|