betterlint 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []