ducalis 0.5.14 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -10
  3. data/README.md +2 -4
  4. data/Rakefile +0 -5
  5. data/bin/ducalis +8 -3
  6. data/config/.ducalis.yml +20 -1
  7. data/ducalis.gemspec +0 -1
  8. data/lib/ducalis.rb +7 -0
  9. data/lib/ducalis/cli.rb +80 -19
  10. data/lib/ducalis/commentators/console.rb +1 -1
  11. data/lib/ducalis/cops/black_list_suffix.rb +3 -0
  12. data/lib/ducalis/cops/callbacks_activerecord.rb +7 -16
  13. data/lib/ducalis/cops/case_mapping.rb +8 -3
  14. data/lib/ducalis/cops/controllers_except.rb +0 -8
  15. data/lib/ducalis/cops/data_access_objects.rb +28 -0
  16. data/lib/ducalis/cops/enforce_namespace.rb +28 -0
  17. data/lib/ducalis/cops/evlis_overusing.rb +30 -0
  18. data/lib/ducalis/cops/extensions/type_resolving.rb +52 -0
  19. data/lib/ducalis/cops/fetch_expression.rb +80 -0
  20. data/lib/ducalis/cops/module_like_class.rb +1 -0
  21. data/lib/ducalis/cops/multiple_times.rb +50 -0
  22. data/lib/ducalis/cops/options_argument.rb +5 -22
  23. data/lib/ducalis/cops/possible_tap.rb +4 -0
  24. data/lib/ducalis/cops/preferable_methods.rb +3 -1
  25. data/lib/ducalis/cops/private_instance_assign.rb +4 -9
  26. data/lib/ducalis/cops/protected_scope_cop.rb +4 -0
  27. data/lib/ducalis/cops/public_send.rb +18 -0
  28. data/lib/ducalis/cops/regex_cop.rb +36 -6
  29. data/lib/ducalis/cops/rest_only_cop.rb +4 -11
  30. data/lib/ducalis/cops/too_long_workers.rb +3 -8
  31. data/lib/ducalis/cops/uncommented_gem.rb +13 -1
  32. data/lib/ducalis/cops/useless_only.rb +6 -8
  33. data/lib/ducalis/documentation.rb +28 -18
  34. data/lib/ducalis/passed_args.rb +2 -2
  35. data/lib/ducalis/version.rb +1 -1
  36. metadata +9 -17
  37. data/DOCUMENTATION.md +0 -1185
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'ducalis/cops/extensions/type_resolving'
5
+
6
+ module Ducalis
7
+ class DataAccessObjects < RuboCop::Cop::Cop
8
+ include RuboCop::Cop::DefNode
9
+ prepend TypeResolving
10
+
11
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
12
+ | It's a good practice to move code related to serialization/deserialization out of the controller. Consider of creating Data Access Object to separate the data access parts from the application logic. It will eliminate problems related to refactoring and testing.
13
+ MESSAGE
14
+
15
+ NODE_EXPRESSIONS = [
16
+ s(:send, nil, :session),
17
+ s(:send, nil, :cookies),
18
+ s(:gvar, :$redis),
19
+ s(:send, s(:const, nil, :Redis), :current)
20
+ ].freeze
21
+
22
+ def on_send(node)
23
+ return unless in_controller?
24
+ return unless NODE_EXPRESSIONS.include?(node.to_a.first)
25
+ add_offense(node, :expression, OFFENSE)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class EnforceNamespace < RuboCop::Cop::Cop
7
+ prepend TypeResolving
8
+
9
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
+ | Too improve code organization it is better to define namespaces to group services by high-level features, domains or any other dimension.
11
+ MESSAGE
12
+
13
+ def on_class(node)
14
+ return if !node.parent.nil? || !in_service?
15
+ add_offense(node, :expression, OFFENSE)
16
+ end
17
+
18
+ def on_module(node)
19
+ return if !node.parent.nil? || !in_service?
20
+ return if contains_class?(node) || contains_classes?(node)
21
+ add_offense(node, :expression, OFFENSE)
22
+ end
23
+
24
+ def_node_search :contains_class?, '(module _ ({casgn module class} ...))'
25
+ def_node_search :contains_classes?,
26
+ '(module _ (begin ({casgn module class} ...) ...))'
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class EvlisOverusing < RuboCop::Cop::Cop
7
+ prepend TypeResolving
8
+
9
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
+ | Seems like you are overusing safe navigation operator. Try to use right method (ex: `dig` for hashes), null object pattern or ensure types via explicit conversion (`to_a`, `to_s` and so on).
11
+ MESSAGE
12
+
13
+ DETAILS = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
14
+ | Related article: https://karolgalanciak.com/blog/2017/09/24/do-or-do-not-there-is-no-try-object-number-try-considered-harmful/
15
+ MESSAGE
16
+
17
+ def on_send(node)
18
+ return unless nested_try?(node)
19
+ add_offense(node, :expression, OFFENSE)
20
+ end
21
+
22
+ def on_csend(node)
23
+ return unless node.child_nodes.any?(&:csend_type?)
24
+ add_offense(node, :expression, OFFENSE)
25
+ end
26
+
27
+ def_node_search :nested_try?,
28
+ '(send (send _ {:try :try!} ...) {:try :try!} ...)'
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ module TypeResolving
3
+ MODELS_CLASS_NAMES = %w(
4
+ ApplicationRecord
5
+ ActiveRecord::Base
6
+ ).freeze
7
+
8
+ WORKERS_SUFFIXES = %w(
9
+ Worker
10
+ Job
11
+ ).freeze
12
+
13
+ CONTROLLER_SUFFIXES = %w(
14
+ Controller
15
+ ).freeze
16
+
17
+ SERVICES_PATH = File.join('app', 'services')
18
+
19
+ def on_class(node)
20
+ classdef_node, superclass, _body = *node
21
+ @node = node
22
+ @class_name = classdef_node.loc.expression.source
23
+ @superclass_name = superclass.loc.expression.source unless superclass.nil?
24
+ super if defined?(super)
25
+ end
26
+
27
+ def on_module(node)
28
+ @node = node
29
+ super if defined?(super)
30
+ end
31
+
32
+ private
33
+
34
+ def in_service?
35
+ path = @node.location.expression.source_buffer.name
36
+ services_path = cop_config.fetch('ServicePath') { SERVICES_PATH }
37
+ path.include?(services_path)
38
+ end
39
+
40
+ def in_controller?
41
+ return false if @superclass_name.nil?
42
+ @superclass_name.end_with?(*CONTROLLER_SUFFIXES)
43
+ end
44
+
45
+ def in_model?
46
+ MODELS_CLASS_NAMES.include?(@superclass_name)
47
+ end
48
+
49
+ def in_worker?
50
+ @class_name.end_with?(*WORKERS_SUFFIXES)
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class FetchExpression < RuboCop::Cop::Cop
7
+ HASH_CALLING_REGEX = /\:\[\]/ # params[:key]
8
+
9
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
+ | You can use `fetch` instead:
11
+
12
+ | ```ruby
13
+ | %<source>s
14
+ | ```
15
+
16
+ MESSAGE
17
+
18
+ def investigate(processed_source)
19
+ return unless processed_source.ast
20
+ matching_nodes(processed_source.ast).each do |node|
21
+ add_offense(node, :expression, format(OFFENSE,
22
+ source: correct_variant(node)))
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def matching_nodes(ast)
29
+ [
30
+ *ternar_gets_present(ast).select(&method(:matching_ternar?)),
31
+ *ternar_gets_nil(ast).select(&method(:matching_ternar?)),
32
+ *default_gets(ast)
33
+ ].uniq
34
+ end
35
+
36
+ def_node_search :default_gets, '(or (send (...) :[] (...)) (...))'
37
+ def_node_search :ternar_gets_present, '(if (...) (send ...) (...))'
38
+ def_node_search :ternar_gets_nil, '(if (send (...) :nil?) (...) (send ...))'
39
+
40
+ def matching_ternar?(node)
41
+ present_matching?(node) || nil_matching?(node)
42
+ end
43
+
44
+ def present_matching?(node)
45
+ source, result, = *node
46
+ (source == result && result.to_s =~ HASH_CALLING_REGEX)
47
+ end
48
+
49
+ def nil_matching?(node)
50
+ source, _, result = *node
51
+ (source.to_a.first == result && result.to_s =~ HASH_CALLING_REGEX)
52
+ end
53
+
54
+ def correct_variant(node)
55
+ if nil_matching?(node)
56
+ nil_correct(node)
57
+ else
58
+ present_correct(node)
59
+ end
60
+ end
61
+
62
+ def nil_correct(node)
63
+ hash, _, key = *node.to_a.last.to_a
64
+ construct_fetch(hash, key, node.to_a[1])
65
+ end
66
+
67
+ def present_correct(node)
68
+ hash, _, key = *node.to_a.first.to_a
69
+ construct_fetch(hash, key, node.to_a.last)
70
+ end
71
+
72
+ def construct_fetch(hash, key, default)
73
+ [source(hash), ".fetch(#{source(key)})", " { #{source(default)} }"].join
74
+ end
75
+
76
+ def source(node)
77
+ node.loc.expression.source
78
+ end
79
+ end
80
+ end
@@ -5,6 +5,7 @@ require 'rubocop'
5
5
  module Ducalis
6
6
  class ModuleLikeClass < RuboCop::Cop::Cop
7
7
  include RuboCop::Cop::DefNode
8
+
8
9
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
9
10
  | Seems like it will be better to define initialize and pass %<args>s there instead of each method.
10
11
  MESSAGE
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class MultipleTimes < RuboCop::Cop::Cop
7
+ include RuboCop::Cop::DefNode
8
+
9
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
+ | You should avoid multiple time-related calls to prevent bugs during the period junctions (like Time.now.day called twice in the same scope could return different values if you called it near 23:59:59). You can pass it as default keyword argument or assign to a local variable.
11
+ MESSAGE
12
+
13
+ DETAILS = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
14
+ | Compare:
15
+
16
+ | ```ruby
17
+ | def period
18
+ | Date.today..(Date.today + 1.day)
19
+ | end
20
+ | # vs
21
+ | def period(today: Date.today)
22
+ | today..(today + 1.day)
23
+ | end
24
+ | ```
25
+
26
+ MESSAGE
27
+
28
+ PARAMS_CALL = s(:send, nil, :params)
29
+
30
+ def on_def(body)
31
+ multiple = [
32
+ date_today(body),
33
+ date_current(body),
34
+ time_current(body),
35
+ time_now(body)
36
+ ].map(&:to_a).compact.flatten.to_a
37
+ return if multiple.count < 2
38
+ multiple.each do |time_node|
39
+ add_offense(time_node, :expression, OFFENSE)
40
+ end
41
+ end
42
+ alias on_defs on_def
43
+ alias on_send on_def
44
+
45
+ def_node_search :date_today, '(send (const nil :Date) :today)'
46
+ def_node_search :date_current, '(send (const nil :Date) :current)'
47
+ def_node_search :time_current, '(send (const nil :Time) :current)'
48
+ def_node_search :time_now, '(send (const nil :Time) :now)'
49
+ end
50
+ end
@@ -6,7 +6,9 @@ module Ducalis
6
6
  class OptionsArgument < RuboCop::Cop::Cop
7
7
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
8
8
  | Default `options` (or `args`) argument isn't good idea. It's better to explicitly pass which keys are you interested in as keyword arguments. You can use split operator to support hash arguments.
9
+ MESSAGE
9
10
 
11
+ DETAILS = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
12
  | Compare:
11
13
 
12
14
  | ```ruby
@@ -31,34 +33,15 @@ module Ducalis
31
33
  | options = { format: 'csv', limit: 5, useless_arg: :value }
32
34
  | generate_2(1, **options) #=> ["csv", 5, {:useless_arg=>:value}]
33
35
  | generate_2(1, format: 'csv', limit: 5, useless_arg: :value) #=> ["csv", 5, {:useless_arg=>:value}]
34
-
35
36
  | ```
36
- MESSAGE
37
37
 
38
- BLACK_LIST = %i(
39
- options
40
- args
41
- ).freeze
38
+ MESSAGE
42
39
 
43
40
  def on_def(node)
44
- _name, args, _body = *node
45
- return unless default_options?(args)
41
+ return unless options_like_arg?(node)
46
42
  add_offense(node, :expression, OFFENSE)
47
43
  end
48
44
 
49
- private
50
-
51
- def_node_search :options_arg?, '(arg #blacklisted?)'
52
- def_node_search :options_arg_with_default?, '(optarg #blacklisted? ...)'
53
-
54
- def default_options?(args)
55
- args.children.any? do |node|
56
- options_arg?(node) || options_arg_with_default?(node)
57
- end
58
- end
59
-
60
- def blacklisted?(name)
61
- BLACK_LIST.include?(name)
62
- end
45
+ def_node_search :options_like_arg?, '(${arg optarg} ${:options :args} ...)'
63
46
  end
64
47
  end
@@ -7,6 +7,10 @@ module Ducalis
7
7
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
8
8
  | Consider of using `.tap`, default ruby [method](<https://apidock.com/ruby/Object/tap>) which allows to replace intermediate variables with block, by this you are limiting scope pollution and make method scope more clear.
9
9
  | If it isn't possible, consider of moving it to method or even inline it.
10
+ |
11
+ MESSAGE
12
+
13
+ DETAILS = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
10
14
  | [Related article](<http://seejohncode.com/2012/01/02/ruby-tap-that/>).
11
15
  MESSAGE
12
16
 
@@ -7,11 +7,13 @@ module Ducalis
7
7
  | Prefer to use %<alternative>s method instead of %<original>s because of %<reason>s.
8
8
  MESSAGE
9
9
 
10
+ WHITE_LIST = %w(cache file params attrs options).freeze
11
+
10
12
  ALWAYS_TRUE = ->(_who, _what, _args) { true }
11
13
 
12
14
  DELETE_CHECK = lambda do |who, _what, args|
13
15
  !%i(sym str).include?(args.first && args.first.type) &&
14
- args.count <= 1 && who.to_s !~ /file/
16
+ args.count <= 1 && !WHITE_LIST.any? { |regex| who.to_s.include?(regex) }
15
17
  end
16
18
 
17
19
  VALIDATE_CHECK = lambda do |_who, _what, args|
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubocop'
4
+ require 'ducalis/cops/extensions/type_resolving'
4
5
 
5
6
  module Ducalis
6
7
  class PrivateInstanceAssign < RuboCop::Cop::Cop
7
8
  include RuboCop::Cop::DefNode
9
+ prepend TypeResolving
10
+
8
11
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
9
12
  | Don't use controller's filter methods for setting instance variables, use them only for changing application flow, such as redirecting if a user is not authenticated. Controller instance variables are forming contract between controller and view. Keeping instance variables defined in one place makes it easier to: reason, refactor and remove old views, test controllers and views, extract actions to new controllers, etc.
10
13
  MESSAGE
@@ -15,14 +18,8 @@ module Ducalis
15
18
 
16
19
  DETAILS = ADD_OFFENSE
17
20
 
18
- def on_class(node)
19
- _classdef_node, superclass, _body = *node
20
- return if superclass.nil?
21
- @triggered = superclass.loc.expression.source =~ /Controller/
22
- end
23
-
24
21
  def on_ivasgn(node)
25
- return unless triggered
22
+ return unless in_controller?
26
23
  return unless non_public?(node)
27
24
  return check_memo(node) if node.parent.type == :or_asgn
28
25
  add_offense(node, :expression, OFFENSE)
@@ -34,7 +31,5 @@ module Ducalis
34
31
  return if node.to_a.first.to_s.start_with?('@_')
35
32
  add_offense(node, :expression, [OFFENSE, ADD_OFFENSE].join(' '))
36
33
  end
37
-
38
- attr_reader :triggered
39
34
  end
40
35
  end
@@ -6,6 +6,9 @@ module Ducalis
6
6
  class ProtectedScopeCop < RuboCop::Cop::Cop
7
7
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
8
8
  | Seems like you are using `find` on non-protected scope. Potentially it could lead to unauthorized access. It's better to call `find` on authorized resources scopes.
9
+ MESSAGE
10
+
11
+ DETAILS = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
9
12
  | Example:
10
13
 
11
14
  | ```ruby
@@ -13,6 +16,7 @@ module Ducalis
13
16
  | # better then
14
17
  | Employee.find(params[:id])
15
18
  | ```
19
+
16
20
  MESSAGE
17
21
 
18
22
  def on_send(node)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module Ducalis
6
+ class PublicSend < RuboCop::Cop::Cop
7
+ OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
8
+ | You should avoid of using `send`-like method in production code. You can rewrite it as a hash with lambdas and fetch necessary actions or rewrite it as a module which you can include in code.
9
+ MESSAGE
10
+
11
+ def on_send(node)
12
+ return unless send_call?(node)
13
+ add_offense(node, :expression, OFFENSE)
14
+ end
15
+
16
+ def_node_matcher :send_call?, '(send _ ${:send :public_send :__send__} ...)'
17
+ end
18
+ end
@@ -5,13 +5,18 @@ require 'regexp-examples'
5
5
 
6
6
  module Ducalis
7
7
  class RegexCop < RuboCop::Cop::Cop
8
+ include RuboCop::Cop::DefNode
9
+
8
10
  OFFENSE = <<-MESSAGE.gsub(/^ +\|\s/, '').strip
9
11
  | It's better to move regex to constants with example instead of direct using it. It will allow you to reuse this regex and provide instructions for others.
10
12
 
13
+ | Example:
14
+
11
15
  | ```ruby
12
16
  | CONST_NAME = %<constant>s # "%<example>s"
13
17
  | %<fixed_string>s
14
18
  | ```
19
+
15
20
  MESSAGE
16
21
 
17
22
  SELF_DESCRIPTIVE = %w(
@@ -34,22 +39,47 @@ module Ducalis
34
39
  DETAILS = "Available regexes are:
35
40
  #{SELF_DESCRIPTIVE.map { |name| "`#{name}`" }.join(', ')}"
36
41
 
37
- def on_regexp(node)
38
- return if node.parent.type == :casgn
39
- return if SELF_DESCRIPTIVE.include?(node.source)
40
- return if node.child_nodes.any? { |child_node| child_node.type == :begin }
41
- add_offense(node, :expression, format(OFFENSE, present_node(node)))
42
+ DEFAULT_EXAMPLE = 'some_example'
43
+
44
+ def on_begin(node)
45
+ not_defined_regexes(node).each do |regex|
46
+ next if SELF_DESCRIPTIVE.include?(regex.source) || const_dynamic?(regex)
47
+ add_offense(regex, :expression, format(OFFENSE, present_node(regex)))
48
+ end
42
49
  end
43
50
 
44
51
  private
45
52
 
53
+ def_node_search :const_using, '(regexp $_ ... (regopt))'
54
+ def_node_search :const_definition, '(casgn ...)'
55
+
56
+ def not_defined_regexes(node)
57
+ const_using(node).reject do |regex|
58
+ defined_as_const?(regex, const_definition(node))
59
+ end.map(&:parent)
60
+ end
61
+
62
+ def defined_as_const?(regex, definitions)
63
+ definitions.any? { |node| const_using(node).any? { |use| use == regex } }
64
+ end
65
+
66
+ def const_dynamic?(node)
67
+ node.child_nodes.any?(&:begin_type?)
68
+ end
69
+
46
70
  def present_node(node)
47
71
  {
48
72
  constant: node.source,
49
73
  fixed_string: node.source_range.source_line
50
74
  .sub(node.source, 'CONST_NAME').lstrip,
51
- example: Regexp.new(node.to_a.first.to_a.first).examples.sample
75
+ example: regex_sample(node)
52
76
  }
53
77
  end
78
+
79
+ def regex_sample(node)
80
+ Regexp.new(node.to_a.first.to_a.first).examples.sample
81
+ rescue RegexpExamples::IllegalSyntaxError
82
+ DEFAULT_EXAMPLE
83
+ end
54
84
  end
55
85
  end