ducalis 0.5.14 → 0.6.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +8 -10
- data/README.md +2 -4
- data/Rakefile +0 -5
- data/bin/ducalis +8 -3
- data/config/.ducalis.yml +20 -1
- data/ducalis.gemspec +0 -1
- data/lib/ducalis.rb +7 -0
- data/lib/ducalis/cli.rb +80 -19
- data/lib/ducalis/commentators/console.rb +1 -1
- data/lib/ducalis/cops/black_list_suffix.rb +3 -0
- data/lib/ducalis/cops/callbacks_activerecord.rb +7 -16
- data/lib/ducalis/cops/case_mapping.rb +8 -3
- data/lib/ducalis/cops/controllers_except.rb +0 -8
- data/lib/ducalis/cops/data_access_objects.rb +28 -0
- data/lib/ducalis/cops/enforce_namespace.rb +28 -0
- data/lib/ducalis/cops/evlis_overusing.rb +30 -0
- data/lib/ducalis/cops/extensions/type_resolving.rb +52 -0
- data/lib/ducalis/cops/fetch_expression.rb +80 -0
- data/lib/ducalis/cops/module_like_class.rb +1 -0
- data/lib/ducalis/cops/multiple_times.rb +50 -0
- data/lib/ducalis/cops/options_argument.rb +5 -22
- data/lib/ducalis/cops/possible_tap.rb +4 -0
- data/lib/ducalis/cops/preferable_methods.rb +3 -1
- data/lib/ducalis/cops/private_instance_assign.rb +4 -9
- data/lib/ducalis/cops/protected_scope_cop.rb +4 -0
- data/lib/ducalis/cops/public_send.rb +18 -0
- data/lib/ducalis/cops/regex_cop.rb +36 -6
- data/lib/ducalis/cops/rest_only_cop.rb +4 -11
- data/lib/ducalis/cops/too_long_workers.rb +3 -8
- data/lib/ducalis/cops/uncommented_gem.rb +13 -1
- data/lib/ducalis/cops/useless_only.rb +6 -8
- data/lib/ducalis/documentation.rb +28 -18
- data/lib/ducalis/passed_args.rb +2 -2
- data/lib/ducalis/version.rb +1 -1
- metadata +9 -17
- 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
|
-
|
39
|
-
options
|
40
|
-
args
|
41
|
-
).freeze
|
38
|
+
MESSAGE
|
42
39
|
|
43
40
|
def on_def(node)
|
44
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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:
|
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
|