betterlint 1.0.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,25 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class SitePrismLoaded < Cop
5
+ MSG = 'Use `be_loaded` instead of `be_displayed`'.freeze
6
+
7
+ def_node_matcher :be_displayed_call?, <<-PATTERN
8
+ (send (send nil? :expect _) _ (send nil? :be_displayed))
9
+ PATTERN
10
+
11
+ def on_send(node)
12
+ return unless be_displayed_call?(node)
13
+
14
+ add_offense(node, location: node.children[2].loc.expression)
15
+ end
16
+
17
+ def autocorrect(node)
18
+ lambda do |corrector|
19
+ corrector.replace(node.children[2].loc.expression, 'be_loaded')
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ # If a file requires spec_helper or rails_helper, make sure
5
+ # it is located in a spec/ directory.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # app/models/whatever_spec.rb
10
+ # require 'rails_helper'
11
+ #
12
+ # # good
13
+ # spec/models/my_class_spec.rb
14
+ # require 'rails_helper'
15
+ class SpecHelperRequiredOutsideSpecDir < Cop
16
+ MSG = 'Spec helper required outside of a spec/ directory.'.freeze
17
+
18
+ def_node_matcher :requires_spec_helper?, <<-PATTERN
19
+ (send nil? :require
20
+ (str {"rails_helper" "spec_helper"}))
21
+ PATTERN
22
+
23
+ def on_send(node)
24
+ add_offense(node) if requires_spec_helper?(node) && !spec_directory?
25
+ end
26
+
27
+ private
28
+
29
+ def spec_directory?
30
+ Pathname.new(processed_source.buffer.name)
31
+ .relative_path_from(Pathname.pwd)
32
+ .to_s
33
+ .start_with?("spec#{File::SEPARATOR}")
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class Timeout < Cop
5
+ MSG = 'Using Timeout.timeout without a custom exception can prevent rescue blocks from executing'.freeze
6
+
7
+ def_node_matcher :timeout_call?, <<-PATTERN
8
+ (send (const nil? :Timeout) :timeout _)
9
+ PATTERN
10
+
11
+ def on_send(node)
12
+ return unless timeout_call?(node)
13
+
14
+ add_offense(node)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class UnsafeJob < Cop
5
+ attr_accessor :sensitive_params, :class_regex
6
+
7
+ MSG = <<~MSG.freeze
8
+ This job takes a parameter that will end up serialized in plaintext. Do not pass sensitive data as bare arguments into jobs.
9
+
10
+ See here for more information on this error:
11
+ https://github.com/Betterment/betterlint#bettermentunsafejob
12
+ MSG
13
+
14
+ def initialize(config = nil, options = nil)
15
+ super(config, options)
16
+ config = @config.for_cop(self)
17
+ @sensitive_params = config.fetch("sensitive_params", []).map(&:to_sym)
18
+ @class_regex = Regexp.new config.fetch("class_regex", ".*Job$")
19
+ end
20
+
21
+ def on_def(node)
22
+ return unless %i(perform initialize).include?(node.method_name)
23
+ return unless @class_regex.match(node.parent_module_name)
24
+
25
+ node.arguments.any? do |argument|
26
+ name, = *argument
27
+ add_offense(argument) if @sensitive_params.include?(name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,87 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class UnscopedFind < Cop
5
+ attr_accessor :unauthenticated_models
6
+
7
+ MSG = <<~MSG.freeze
8
+ Records are being retrieved directly using user input.
9
+ Please query for the associated record in a way that enforces authorization (e.g. "trust-root chaining").
10
+
11
+ INSTEAD OF THIS:
12
+ Post.find(params[:post_id])
13
+
14
+ DO THIS:
15
+ current_user.posts.find(params[:post_id])
16
+
17
+ See here for more information on this error:
18
+ https://github.com/Betterment/betterlint/blob/main/README.md#bettermentunscopedfind
19
+ MSG
20
+ METHOD_PATTERN = /^find_by_(.+?)(!)?$/.freeze
21
+ FINDS = %i(find find_by find_by! where).freeze
22
+
23
+ def_node_matcher :custom_scope_find?, <<-PATTERN
24
+ (send (send (const ... _) ...) {#{FINDS.map(&:inspect).join(' ')}} ...)
25
+ PATTERN
26
+
27
+ def_node_matcher :find?, <<-PATTERN
28
+ (send (const ... _) {#{FINDS.map(&:inspect).join(' ')}} ...)
29
+ PATTERN
30
+
31
+ def initialize(config = nil, options = nil)
32
+ super(config, options)
33
+ config = @config.for_cop(self)
34
+ @unauthenticated_models = config.fetch("unauthenticated_models", []).map(&:to_sym)
35
+ end
36
+
37
+ def on_class(node)
38
+ Utils::MethodReturnTable.populate_index(node)
39
+ end
40
+
41
+ def on_send(node)
42
+ _, _, *arg_nodes = *node # rubocop:disable InternalAffairs/NodeDestructuring
43
+ return unless
44
+ (
45
+ find?(node) ||
46
+ custom_scope_find?(node) ||
47
+ static_method_name(node.method_name)
48
+ ) && !@unauthenticated_models.include?(Utils::Parser.get_root_token(node))
49
+
50
+ add_offense(node) if find_param_arg(arg_nodes)
51
+ end
52
+
53
+ private
54
+
55
+ def find_param_arg(arg_nodes)
56
+ return unless arg_nodes
57
+
58
+ arg_nodes.find do |arg|
59
+ if arg.hash_type?
60
+ arg.children.each do |pair|
61
+ _key, value = *pair.children
62
+ return arg if uses_params?(value)
63
+ end
64
+ end
65
+
66
+ uses_params?(arg)
67
+ end
68
+ end
69
+
70
+ def uses_params?(node)
71
+ root = Utils::Parser.get_root_token(node)
72
+ root == :params || Array(Utils::MethodReturnTable.get_method(root)).find do |x|
73
+ Utils::Parser.get_root_token(x) == :params
74
+ end
75
+ end
76
+
77
+ # yoinked from Rails/DynamicFindBy
78
+ def static_method_name(method_name)
79
+ match = METHOD_PATTERN.match(method_name)
80
+ return nil unless match
81
+
82
+ match[2] ? 'find_by!' : 'find_by'
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ module MethodReturnTable
5
+ class << self
6
+ def populate_index(node)
7
+ raise "not a class" unless node.class_type?
8
+
9
+ get_methods_for_class(node).each do |method|
10
+ track_method(method.method_name, Utils::Parser.get_return_values(method))
11
+ end
12
+
13
+ node.descendants.each do |descendant|
14
+ lhs, rhs = *descendant
15
+ next unless descendant.equals_asgn? && (descendant.type != :casgn) && rhs&.send_type?
16
+
17
+ track_method(lhs, [rhs])
18
+ end
19
+ end
20
+
21
+ def indexed_methods
22
+ @indexed_methods ||= {}
23
+ end
24
+
25
+ def get_method(method_name)
26
+ indexed_methods[method_name]
27
+ end
28
+
29
+ def has_method?(method_name)
30
+ indexed_methods.include?(method_name)
31
+ end
32
+
33
+ private
34
+
35
+ def track_method(method_name, returns)
36
+ indexed_methods[method_name] = returns
37
+ end
38
+
39
+ def get_methods_for_class(node)
40
+ return [] unless node.children && node.class_type?
41
+
42
+ node.descendants.select(&:def_type?)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,115 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ module Parser
5
+ def self.get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
6
+ return nil unless node
7
+
8
+ return get_root_token(node.receiver) if node.receiver
9
+
10
+ # rubocop:disable InternalAffairs/NodeDestructuring
11
+ if node.send_type?
12
+ name = node.method_name
13
+ elsif node.variable?
14
+ name, = *node
15
+ elsif node.literal?
16
+ _, name = *node
17
+ elsif node.const_type?
18
+ name = node.const_name.to_sym
19
+ elsif node.sym_type?
20
+ name = node.value
21
+ elsif node.self_type?
22
+ name = :self
23
+ elsif node.block_pass_type?
24
+ name, = *node.children
25
+ else
26
+ name = nil
27
+ end
28
+ # rubocop:enable InternalAffairs/NodeDestructuring
29
+
30
+ name
31
+ end
32
+
33
+ def self.get_return_values(node) # rubocop:disable Metrics/AbcSize
34
+ return [] unless node
35
+ return explicit_returns(node) + get_return_values(node.body) if node.def_type?
36
+ return [node] if node.literal? || node.variable?
37
+
38
+ case node.type
39
+ when :begin
40
+ get_return_values(node.children.last)
41
+ when :block
42
+ get_return_values(node.body)
43
+ when :if
44
+ if_rets = get_return_values(node.if_branch)
45
+ else_rets = get_return_values(node.else_branch)
46
+ if_rets + else_rets
47
+ when :case
48
+ cases = []
49
+ node.each_when do |block|
50
+ cases += get_return_values(block.body)
51
+ end
52
+
53
+ cases + get_return_values(node.else_branch)
54
+ when :send
55
+ [node]
56
+ else
57
+ []
58
+ end
59
+ end
60
+
61
+ def self.explicit_returns(node)
62
+ node.descendants.select(&:return_type?).map { |x|
63
+ x&.children&.first
64
+ }.compact
65
+ end
66
+
67
+ def self.params_from_arguments(arguments) # rubocop:disable Metrics/PerceivedComplexity
68
+ parameter_names = []
69
+
70
+ arguments.each do |arg|
71
+ if arg.hash_type?
72
+ arg.children.each do |pair|
73
+ value = pair.value
74
+ parameter_names << value.value if value.sym_type? || value.str_type?
75
+ end
76
+ elsif arg.sym_type? || arg.str_type?
77
+ parameter_names << arg.value
78
+ end
79
+ end
80
+
81
+ parameter_names
82
+ end
83
+
84
+ def self.get_extracted_parameters(node, param_aliases: []) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
85
+ return [] unless node.send_type?
86
+
87
+ parameter_names = []
88
+ param_aliases << :params
89
+
90
+ if node.method?(:[]) && param_aliases.include?(get_root_token(node))
91
+ return node.arguments.select { |x|
92
+ x.sym_type? || x.str_type?
93
+ }.map(&:value)
94
+ end
95
+
96
+ children = node.descendants.select do |child|
97
+ child.send_type? && param_aliases.include?(child.method_name)
98
+ end
99
+
100
+ children.each do |child|
101
+ ancestors = child.ancestors.select do |ancestor|
102
+ ancestor.send_type? && ancestor.method?(:permit)
103
+ end
104
+
105
+ ancestors.each do |ancestor|
106
+ parameter_names += params_from_arguments(ancestor.arguments)
107
+ end
108
+ end
109
+
110
+ parameter_names.map(&:to_sym)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: betterlint
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Development
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubocop
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop-performance
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec-rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Betterment rubocop configuration
126
+ email:
127
+ - development@betterment.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - README.md
133
+ - STYLEGUIDE.md
134
+ - config/default.yml
135
+ - lib/rubocop/cop/betterment.rb
136
+ - lib/rubocop/cop/betterment/active_job_performable.rb
137
+ - lib/rubocop/cop/betterment/allowlist_blocklist.rb
138
+ - lib/rubocop/cop/betterment/authorization_in_controller.rb
139
+ - lib/rubocop/cop/betterment/dynamic_params.rb
140
+ - lib/rubocop/cop/betterment/implicit_redirect_type.rb
141
+ - lib/rubocop/cop/betterment/memoization_with_arguments.rb
142
+ - lib/rubocop/cop/betterment/server_error_assertion.rb
143
+ - lib/rubocop/cop/betterment/site_prism_loaded.rb
144
+ - lib/rubocop/cop/betterment/spec_helper_required_outside_spec_dir.rb
145
+ - lib/rubocop/cop/betterment/timeout.rb
146
+ - lib/rubocop/cop/betterment/unsafe_job.rb
147
+ - lib/rubocop/cop/betterment/unscoped_find.rb
148
+ - lib/rubocop/cop/betterment/utils/method_return_table.rb
149
+ - lib/rubocop/cop/betterment/utils/parser.rb
150
+ homepage:
151
+ licenses:
152
+ - MIT
153
+ metadata: {}
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '2.4'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.2.3
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: Betterment rubocop configuration
173
+ test_files: []