dry-auto_inject 1.0.1 → 1.2.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/CHANGELOG.md +116 -82
- data/LICENSE +1 -1
- data/README.md +13 -13
- data/config/default.yml +21 -0
- data/dry-auto_inject.gemspec +13 -11
- data/lib/dry/auto_inject/builder.rb +3 -7
- data/lib/dry/auto_inject/dependency_map.rb +6 -10
- data/lib/dry/auto_inject/injector.rb +4 -10
- data/lib/dry/auto_inject/method_parameters.rb +23 -24
- data/lib/dry/auto_inject/strategies/args.rb +11 -8
- data/lib/dry/auto_inject/strategies/hash.rb +8 -6
- data/lib/dry/auto_inject/strategies/kwargs.rb +15 -9
- data/lib/dry/auto_inject/strategies.rb +2 -2
- data/lib/dry/auto_inject/version.rb +1 -1
- data/lib/dry/auto_inject.rb +5 -4
- data/lib/dry-auto_inject.rb +6 -0
- data/lib/rubocop/cop/dry_auto_inject/dependency_order.rb +126 -0
- data/lib/rubocop/cop/dry_auto_inject/mixin.rb +163 -0
- data/lib/rubocop/cop/dry_auto_inject/redundant_alias.rb +68 -0
- data/lib/rubocop/dry-auto_inject.rb +3 -0
- data/lib/rubocop/dry_auto_inject/plugin.rb +34 -0
- data/lib/rubocop/dry_auto_inject.rb +5 -0
- metadata +22 -14
|
@@ -9,12 +9,14 @@ module Dry
|
|
|
9
9
|
|
|
10
10
|
def define_new
|
|
11
11
|
class_mod.class_exec(container, dependency_map) do |container, dependency_map|
|
|
12
|
+
deps_with_indices = dependency_map.to_h.values.map.with_index
|
|
13
|
+
|
|
12
14
|
define_method :new do |*args|
|
|
13
|
-
deps =
|
|
15
|
+
deps = deps_with_indices.map do |identifier, i|
|
|
14
16
|
args[i] || container[identifier]
|
|
15
|
-
|
|
17
|
+
end
|
|
16
18
|
|
|
17
|
-
super(*deps, *args
|
|
19
|
+
super(*deps, *args.drop(deps.size))
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -46,11 +48,12 @@ module Dry
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def define_initialize_with_splat(super_parameters)
|
|
49
|
-
super_pass =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
super_pass =
|
|
52
|
+
if super_parameters.splat?
|
|
53
|
+
"*args"
|
|
54
|
+
else
|
|
55
|
+
"*args.take(#{super_parameters.length})"
|
|
56
|
+
end
|
|
54
57
|
|
|
55
58
|
assignments = dependency_map.names.map.with_index do |name, idx|
|
|
56
59
|
"@#{name} = args[#{idx}]"
|
|
@@ -9,12 +9,14 @@ module Dry
|
|
|
9
9
|
|
|
10
10
|
def define_new
|
|
11
11
|
class_mod.class_exec(container, dependency_map) do |container, dependency_map|
|
|
12
|
+
deps_map = dependency_map.to_h
|
|
13
|
+
|
|
12
14
|
define_method :new do |options = {}|
|
|
13
|
-
deps =
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
deps = deps_map.transform_values do |identifier|
|
|
16
|
+
options[identifier] || container[identifier]
|
|
17
|
+
end
|
|
16
18
|
|
|
17
|
-
super(deps)
|
|
19
|
+
super({**deps, **options})
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -24,7 +26,7 @@ module Dry
|
|
|
24
26
|
super_pass = super_params.empty? ? "" : "options"
|
|
25
27
|
assignments = dependency_map.names.map do |name|
|
|
26
28
|
<<~RUBY
|
|
27
|
-
|
|
29
|
+
if options.key?(:#{name}) || !instance_variable_defined?(:'@#{name}')
|
|
28
30
|
@#{name} = options[:#{name}]
|
|
29
31
|
end
|
|
30
32
|
RUBY
|
|
@@ -33,7 +35,7 @@ module Dry
|
|
|
33
35
|
|
|
34
36
|
instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
35
37
|
def initialize(options) # def initialize(options)
|
|
36
|
-
#
|
|
38
|
+
# if options.key?(:dep) || !instance_variable_defined?(:@dep)
|
|
37
39
|
#{body} # @dep = options[:dep]
|
|
38
40
|
# end
|
|
39
41
|
super(#{super_pass}) # super(options)
|
|
@@ -23,8 +23,10 @@ module Dry
|
|
|
23
23
|
|
|
24
24
|
def define_initialize(klass)
|
|
25
25
|
super_parameters = MethodParameters.of(klass, :initialize).each do |ps|
|
|
26
|
-
# Look upwards past
|
|
27
|
-
#
|
|
26
|
+
# Look upwards past methods that only forward arguments, stopping at the
|
|
27
|
+
# first instance of explicit parameters. So `foo(**kwargs)` is
|
|
28
|
+
# skipped, but `foo(bar:, **kwargs)` is not. `foo(...)` is used by ROM as
|
|
29
|
+
# a delegation pattern, so it is not skipped.
|
|
28
30
|
break ps unless ps.pass_through?
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -37,15 +39,18 @@ module Dry
|
|
|
37
39
|
self
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# rubocop:disable Lint/UnderscorePrefixedVariableName
|
|
40
43
|
def define_initialize_with_keywords(super_parameters)
|
|
41
44
|
assign_dependencies = method(:assign_dependencies)
|
|
42
45
|
slice_kwargs = method(:slice_kwargs)
|
|
43
46
|
|
|
44
47
|
instance_mod.class_exec do
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
# The choice of `__auto_inject_kwargs__` here is intentional, and used
|
|
49
|
+
# by MethodParameters#pass_through? to detect an injected initialize.
|
|
50
|
+
define_method :initialize do |**__auto_inject_kwargs__, &block|
|
|
51
|
+
assign_dependencies.(__auto_inject_kwargs__, self)
|
|
47
52
|
|
|
48
|
-
super_kwargs = slice_kwargs.(
|
|
53
|
+
super_kwargs = slice_kwargs.(__auto_inject_kwargs__, super_parameters)
|
|
49
54
|
|
|
50
55
|
if super_kwargs.any?
|
|
51
56
|
super(**super_kwargs, &block)
|
|
@@ -61,13 +66,13 @@ module Dry
|
|
|
61
66
|
slice_kwargs = method(:slice_kwargs)
|
|
62
67
|
|
|
63
68
|
instance_mod.class_exec do
|
|
64
|
-
define_method :initialize do |*args, **
|
|
65
|
-
assign_dependencies.(
|
|
69
|
+
define_method :initialize do |*args, **__auto_inject_kwargs__, &block|
|
|
70
|
+
assign_dependencies.(__auto_inject_kwargs__, self)
|
|
66
71
|
|
|
67
72
|
if super_parameters.splat?
|
|
68
|
-
super(*args, **
|
|
73
|
+
super(*args, **__auto_inject_kwargs__, &block)
|
|
69
74
|
else
|
|
70
|
-
super_kwargs = slice_kwargs.(
|
|
75
|
+
super_kwargs = slice_kwargs.(__auto_inject_kwargs__, super_parameters)
|
|
71
76
|
|
|
72
77
|
if super_kwargs.any?
|
|
73
78
|
super(*args, **super_kwargs, &block)
|
|
@@ -78,6 +83,7 @@ module Dry
|
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
end
|
|
86
|
+
# rubocop:enable Lint/UnderscorePrefixedVariableName
|
|
81
87
|
|
|
82
88
|
def assign_dependencies(kwargs, destination)
|
|
83
89
|
dependency_map.names.each do |name|
|
|
@@ -11,9 +11,9 @@ module Dry
|
|
|
11
11
|
register :default, strategy
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
register :args, proc { Args }
|
|
14
|
+
register :args, proc { Strategies::Args }
|
|
15
15
|
register :hash, proc { Strategies::Hash }
|
|
16
|
-
register_default :kwargs, proc { Kwargs }
|
|
16
|
+
register_default :kwargs, proc { Strategies::Kwargs }
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
data/lib/dry/auto_inject.rb
CHANGED
|
@@ -6,14 +6,15 @@ require "dry/core"
|
|
|
6
6
|
module Dry
|
|
7
7
|
module AutoInject
|
|
8
8
|
def self.loader
|
|
9
|
-
@loader ||= Zeitwerk::Loader.new.tap do |loader|
|
|
10
|
-
root = File.expand_path("..", __dir__)
|
|
9
|
+
@loader ||= ::Zeitwerk::Loader.new.tap do |loader|
|
|
10
|
+
root = ::File.expand_path("..", __dir__)
|
|
11
11
|
loader.tag = "dry-auto_inject"
|
|
12
|
-
loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-auto_inject.rb")
|
|
12
|
+
loader.inflector = ::Zeitwerk::GemInflector.new("#{root}/dry-auto_inject.rb")
|
|
13
13
|
loader.push_dir(root)
|
|
14
14
|
loader.ignore(
|
|
15
15
|
"#{root}/dry-auto_inject.rb",
|
|
16
|
-
"#{root}/dry/auto_inject/version.rb"
|
|
16
|
+
"#{root}/dry/auto_inject/version.rb",
|
|
17
|
+
"#{root}/rubocop"
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
end
|
data/lib/dry-auto_inject.rb
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mixin"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module DryAutoInject
|
|
8
|
+
# Enforces a configurable order for dry-auto_inject `Import[...]` deps.
|
|
9
|
+
#
|
|
10
|
+
# Non-aliased deps are emitted first, then aliased deps. Within each
|
|
11
|
+
# section, deps are grouped by matching the configured `Order` patterns
|
|
12
|
+
# and sorted alphabetically inside a group. Each pattern can be:
|
|
13
|
+
#
|
|
14
|
+
# - `'*'` — catch-all (at most one; implicitly appended)
|
|
15
|
+
# - `'prefix.*'` — prefix wildcard (matches `prefix` and `prefix.*`)
|
|
16
|
+
# - `'/regex/flags'` — regex matched against the dep path
|
|
17
|
+
# - otherwise — exact path match
|
|
18
|
+
#
|
|
19
|
+
# If `Order` is empty or omits `'*'`, a catch-all `'*'` group is appended
|
|
20
|
+
# implicitly so unmatched deps still have a home.
|
|
21
|
+
#
|
|
22
|
+
# Specific patterns take priority over the `*` catch-all regardless of
|
|
23
|
+
# their position in `Order`.
|
|
24
|
+
#
|
|
25
|
+
# @example Order: ['web.*', '*', 'core.*']
|
|
26
|
+
# # bad
|
|
27
|
+
# include Import[
|
|
28
|
+
# 'core.db',
|
|
29
|
+
# 'web.router',
|
|
30
|
+
# ]
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# include Import[
|
|
34
|
+
# 'web.router',
|
|
35
|
+
# 'core.db',
|
|
36
|
+
# ]
|
|
37
|
+
#
|
|
38
|
+
# @example Order: ['/\A[^.]+\z/', '*'] — group dotless deps first
|
|
39
|
+
# include Import[
|
|
40
|
+
# 'logger',
|
|
41
|
+
# 'core.db',
|
|
42
|
+
# ]
|
|
43
|
+
class DependencyOrder < Base
|
|
44
|
+
extend AutoCorrector
|
|
45
|
+
include Mixin
|
|
46
|
+
|
|
47
|
+
MSG = "Dependencies are not in the configured order."
|
|
48
|
+
|
|
49
|
+
def initialize(config = nil, options = nil)
|
|
50
|
+
super
|
|
51
|
+
@parsed_order = build_parsed_order
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_send(node)
|
|
55
|
+
return unless injector_call?(node)
|
|
56
|
+
|
|
57
|
+
deps = parse_injector_deps(node)
|
|
58
|
+
return if deps.nil?
|
|
59
|
+
|
|
60
|
+
order = parsed_order
|
|
61
|
+
current = deps[:non_aliased] + deps[:aliased]
|
|
62
|
+
sorted = [deps[:non_aliased], deps[:aliased]].flat_map { |group|
|
|
63
|
+
group_and_sort(group, order) { |d| d[:path] }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return if current == sorted
|
|
67
|
+
|
|
68
|
+
add_offense(node, message: MSG) do |corrector|
|
|
69
|
+
replace_injector_content(corrector, node, sorted)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :parsed_order
|
|
76
|
+
|
|
77
|
+
def build_parsed_order
|
|
78
|
+
raw = Array(cop_config["Order"]).dup
|
|
79
|
+
|
|
80
|
+
if raw.count("*") > 1
|
|
81
|
+
raise "#{self.class.badge}: `Order` may contain at most one '*' entry " \
|
|
82
|
+
"(got #{raw.inspect})"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
raw << "*" unless raw.include?("*")
|
|
86
|
+
raw.each { |pat| validate_regex_pattern!(pat) if pat.start_with?("/") }
|
|
87
|
+
raw
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_regex_pattern!(pat)
|
|
91
|
+
return if parse_regex_entry(pat)
|
|
92
|
+
|
|
93
|
+
raise "#{self.class.badge}: invalid regex in `Order` entry #{pat.inspect}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def group_and_sort(deps, order, &)
|
|
97
|
+
groups = ::Hash.new { |h, k| h[k] = [] }
|
|
98
|
+
|
|
99
|
+
# Specific patterns take priority over the `*` catch-all regardless
|
|
100
|
+
# of their position in `Order`.
|
|
101
|
+
specific_patterns = order.reject { |p| p == "*" }
|
|
102
|
+
|
|
103
|
+
deps.each do |d|
|
|
104
|
+
pat = specific_patterns.find { |p| match_pattern?(d[:path], p) } || "*"
|
|
105
|
+
groups[pat] << d
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
order.flat_map { |p| groups[p].sort_by(&) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def match_pattern?(path, pattern)
|
|
112
|
+
return true if pattern == "*"
|
|
113
|
+
|
|
114
|
+
if pattern.start_with?("/")
|
|
115
|
+
parse_regex_entry(pattern)&.match?(path)
|
|
116
|
+
elsif pattern.end_with?(".*")
|
|
117
|
+
prefix = pattern[...-2]
|
|
118
|
+
path == prefix || path.start_with?("#{prefix}.")
|
|
119
|
+
else
|
|
120
|
+
path == pattern
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/core"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module DryAutoInject
|
|
8
|
+
# Shared helpers for cops that inspect `Import[...]`-style calls produced
|
|
9
|
+
# by dry-auto_inject.
|
|
10
|
+
module Mixin
|
|
11
|
+
include ::Dry::Core::Constants
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def injector_call?(node)
|
|
16
|
+
return false unless node.send_type?
|
|
17
|
+
return false unless node.method?(:[])
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return false unless receiver&.const_type?
|
|
21
|
+
|
|
22
|
+
match_injector_module?(receiver)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def match_injector_module?(const_node)
|
|
26
|
+
full = const_fullname(const_node)
|
|
27
|
+
clean = full.sub(/\A::/, "")
|
|
28
|
+
|
|
29
|
+
Array(cop_config["InjectorModules"]).any? do |entry|
|
|
30
|
+
match_injector_entry?(entry.to_s, full, clean)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Entry formats:
|
|
35
|
+
# - `/pattern/flags` — regex literal, matched against `clean`
|
|
36
|
+
# - `*::Name` — last segment match with any prefix (incl. empty)
|
|
37
|
+
# - otherwise — exact match on the full or leading-`::`-stripped path
|
|
38
|
+
def match_injector_entry?(entry, full, clean)
|
|
39
|
+
if entry.start_with?("/")
|
|
40
|
+
regex = parse_regex_entry(entry)
|
|
41
|
+
return false unless regex
|
|
42
|
+
|
|
43
|
+
regex.match?(clean)
|
|
44
|
+
elsif entry.start_with?("*::")
|
|
45
|
+
suffix = entry[3..]
|
|
46
|
+
clean == suffix || clean.end_with?("::#{suffix}")
|
|
47
|
+
else
|
|
48
|
+
entry == full || entry == clean
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns a Regexp for a `/pattern/flags` literal, or nil if the
|
|
53
|
+
# string is not a regex literal or the pattern is invalid.
|
|
54
|
+
def parse_regex_entry(str)
|
|
55
|
+
m = str.match(%r{\A/(.*)/([imx]*)\z}m)
|
|
56
|
+
return nil unless m
|
|
57
|
+
|
|
58
|
+
opts = 0
|
|
59
|
+
opts |= ::Regexp::IGNORECASE if m[2].include?("i")
|
|
60
|
+
opts |= ::Regexp::MULTILINE if m[2].include?("m")
|
|
61
|
+
opts |= ::Regexp::EXTENDED if m[2].include?("x")
|
|
62
|
+
::Regexp.new(m[1], opts)
|
|
63
|
+
rescue ::RegexpError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def const_fullname(const_node)
|
|
68
|
+
parts = []
|
|
69
|
+
current = const_node
|
|
70
|
+
|
|
71
|
+
while current
|
|
72
|
+
if current.const_type?
|
|
73
|
+
parts.unshift(current.children[1].to_s)
|
|
74
|
+
current = current.children[0]
|
|
75
|
+
elsif current.cbase_type?
|
|
76
|
+
parts.unshift("")
|
|
77
|
+
current = nil
|
|
78
|
+
else
|
|
79
|
+
return ""
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
parts.join("::")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Parses the arguments of an `Import[...]` call into a structured form.
|
|
87
|
+
# Returns nil if any argument is not a plain string or symbol-keyed/string-valued pair.
|
|
88
|
+
def parse_injector_deps(node)
|
|
89
|
+
args = node.arguments
|
|
90
|
+
return nil if args.empty?
|
|
91
|
+
|
|
92
|
+
hash_arg = args.last if args.last.hash_type?
|
|
93
|
+
string_nodes = hash_arg ? args[...-1] : args
|
|
94
|
+
|
|
95
|
+
non_aliased = parse_non_aliased_deps(string_nodes)
|
|
96
|
+
aliased = parse_aliased_deps(hash_arg)
|
|
97
|
+
return nil if non_aliased.nil? || aliased.nil?
|
|
98
|
+
|
|
99
|
+
{non_aliased:, aliased:}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_non_aliased_deps(nodes)
|
|
103
|
+
return nil unless nodes.all?(&:str_type?)
|
|
104
|
+
|
|
105
|
+
nodes.map { |arg| {node: arg, alias: nil, path: arg.value} }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_aliased_deps(hash_arg)
|
|
109
|
+
return EMPTY_ARRAY unless hash_arg
|
|
110
|
+
return nil unless hash_arg.pairs.all? { |p| p.key.sym_type? && p.value.str_type? }
|
|
111
|
+
|
|
112
|
+
hash_arg.pairs.map { |p| {node: p, alias: p.key.value.to_s, path: p.value.value} }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def format_dep(dep)
|
|
116
|
+
node = dep[:node]
|
|
117
|
+
if dep[:alias]
|
|
118
|
+
"#{dep[:alias]}: #{node.value.source}"
|
|
119
|
+
else
|
|
120
|
+
node.source
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def line_indent_col(node)
|
|
125
|
+
line = node.source_range.source_line
|
|
126
|
+
line.index(/\S/) || node.source_range.column
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def item_indent_spaces(node)
|
|
130
|
+
args = node.arguments
|
|
131
|
+
|
|
132
|
+
if args.any? && args.first.source_range.line > node.source_range.line
|
|
133
|
+
indent(args.first.source_range.column)
|
|
134
|
+
else
|
|
135
|
+
indent(line_indent_col(node) + 2)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def indent(width)
|
|
140
|
+
" " * width
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def render_deps(node, deps)
|
|
144
|
+
if node.multiline?
|
|
145
|
+
inner_indent = item_indent_spaces(node)
|
|
146
|
+
close_indent = indent(line_indent_col(node))
|
|
147
|
+
lines = deps.map { |d| "#{inner_indent}#{format_dep(d)}," }
|
|
148
|
+
"\n#{lines.join("\n")}\n#{close_indent}"
|
|
149
|
+
else
|
|
150
|
+
deps.map { |d| format_dep(d) }.join(", ")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def replace_injector_content(corrector, node, deps)
|
|
155
|
+
selector = node.loc.selector
|
|
156
|
+
return unless selector&.source&.start_with?("[")
|
|
157
|
+
|
|
158
|
+
corrector.replace(selector.adjust(begin_pos: 1, end_pos: -1), render_deps(node, deps))
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mixin"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module DryAutoInject
|
|
8
|
+
# Flags dry-auto_inject imports whose hash key matches the last segment
|
|
9
|
+
# of the path, since dry-auto_inject already derives the dependency name
|
|
10
|
+
# from that segment.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# include Import[foo: 'some.path.foo']
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# include Import['some.path.foo']
|
|
18
|
+
class RedundantAlias < Base
|
|
19
|
+
extend AutoCorrector
|
|
20
|
+
include Mixin
|
|
21
|
+
|
|
22
|
+
MSG =
|
|
23
|
+
"Redundant alias `%<alias_name>s:` — dependency key is derived from " \
|
|
24
|
+
"the last segment of `'%<path>s'`."
|
|
25
|
+
|
|
26
|
+
def on_send(node)
|
|
27
|
+
return unless injector_call?(node)
|
|
28
|
+
|
|
29
|
+
deps = parse_injector_deps(node)
|
|
30
|
+
return if deps.nil?
|
|
31
|
+
|
|
32
|
+
redundant = deps[:aliased].select { |d| d[:alias] == d[:path].split(".").last }
|
|
33
|
+
return if redundant.empty?
|
|
34
|
+
|
|
35
|
+
canonical = promote_redundant_aliases(deps)
|
|
36
|
+
|
|
37
|
+
redundant.each_with_index do |dep, i|
|
|
38
|
+
add_offense(
|
|
39
|
+
dep[:node],
|
|
40
|
+
message: format(MSG, alias_name: dep[:alias], path: dep[:path])
|
|
41
|
+
) do |corrector|
|
|
42
|
+
next unless i.zero?
|
|
43
|
+
|
|
44
|
+
replace_injector_content(corrector, node, canonical)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def promote_redundant_aliases(deps)
|
|
52
|
+
non_aliased = deps[:non_aliased].dup
|
|
53
|
+
aliased = []
|
|
54
|
+
|
|
55
|
+
deps[:aliased].each do |d|
|
|
56
|
+
if d[:alias] == d[:path].split(".").last
|
|
57
|
+
non_aliased << {node: d[:node].value, alias: nil, path: d[:path]}
|
|
58
|
+
else
|
|
59
|
+
aliased << d
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
non_aliased + aliased
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
|
|
5
|
+
require "dry/auto_inject/version"
|
|
6
|
+
|
|
7
|
+
module RuboCop
|
|
8
|
+
module DryAutoInject
|
|
9
|
+
class Plugin < ::LintRoller::Plugin
|
|
10
|
+
def about
|
|
11
|
+
::LintRoller::About.new(
|
|
12
|
+
name: "dry-auto_inject",
|
|
13
|
+
version: ::Dry::AutoInject::VERSION,
|
|
14
|
+
homepage: "https://hanakai.org/learn/dry/dry-auto_inject",
|
|
15
|
+
description: "RuboCop cops for enforcing dry-auto_inject conventions."
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def supported?(context)
|
|
20
|
+
context.engine == :rubocop
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def rules(_context)
|
|
24
|
+
project_root = ::Pathname.new(__dir__).join("../../..")
|
|
25
|
+
|
|
26
|
+
::LintRoller::Rules.new(
|
|
27
|
+
type: :path,
|
|
28
|
+
config_format: :rubocop,
|
|
29
|
+
value: project_root.join("config", "default.yml")
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dry-auto_inject
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
bindir: bin
|
|
7
|
+
- Hanakai team
|
|
8
|
+
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: dry-core
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.
|
|
18
|
+
version: '1.1'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.
|
|
25
|
+
version: '1.1'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: zeitwerk
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -82,14 +81,18 @@ dependencies:
|
|
|
82
81
|
version: '0'
|
|
83
82
|
description: Container-agnostic automatic constructor injection
|
|
84
83
|
email:
|
|
85
|
-
-
|
|
84
|
+
- info@hanakai.org
|
|
86
85
|
executables: []
|
|
87
86
|
extensions: []
|
|
88
|
-
extra_rdoc_files:
|
|
87
|
+
extra_rdoc_files:
|
|
88
|
+
- CHANGELOG.md
|
|
89
|
+
- LICENSE
|
|
90
|
+
- README.md
|
|
89
91
|
files:
|
|
90
92
|
- CHANGELOG.md
|
|
91
93
|
- LICENSE
|
|
92
94
|
- README.md
|
|
95
|
+
- config/default.yml
|
|
93
96
|
- dry-auto_inject.gemspec
|
|
94
97
|
- lib/dry-auto_inject.rb
|
|
95
98
|
- lib/dry/auto_inject.rb
|
|
@@ -103,15 +106,21 @@ files:
|
|
|
103
106
|
- lib/dry/auto_inject/strategies/hash.rb
|
|
104
107
|
- lib/dry/auto_inject/strategies/kwargs.rb
|
|
105
108
|
- lib/dry/auto_inject/version.rb
|
|
109
|
+
- lib/rubocop/cop/dry_auto_inject/dependency_order.rb
|
|
110
|
+
- lib/rubocop/cop/dry_auto_inject/mixin.rb
|
|
111
|
+
- lib/rubocop/cop/dry_auto_inject/redundant_alias.rb
|
|
112
|
+
- lib/rubocop/dry-auto_inject.rb
|
|
113
|
+
- lib/rubocop/dry_auto_inject.rb
|
|
114
|
+
- lib/rubocop/dry_auto_inject/plugin.rb
|
|
106
115
|
homepage: https://dry-rb.org/gems/dry-auto_inject
|
|
107
116
|
licenses:
|
|
108
117
|
- MIT
|
|
109
118
|
metadata:
|
|
110
|
-
allowed_push_host: https://rubygems.org
|
|
111
119
|
changelog_uri: https://github.com/dry-rb/dry-auto_inject/blob/main/CHANGELOG.md
|
|
112
120
|
source_code_uri: https://github.com/dry-rb/dry-auto_inject
|
|
113
121
|
bug_tracker_uri: https://github.com/dry-rb/dry-auto_inject/issues
|
|
114
|
-
|
|
122
|
+
funding_uri: https://github.com/sponsors/hanami
|
|
123
|
+
default_lint_roller_plugin: RuboCop::DryAutoInject::Plugin
|
|
115
124
|
rdoc_options: []
|
|
116
125
|
require_paths:
|
|
117
126
|
- lib
|
|
@@ -119,15 +128,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
119
128
|
requirements:
|
|
120
129
|
- - ">="
|
|
121
130
|
- !ruby/object:Gem::Version
|
|
122
|
-
version:
|
|
131
|
+
version: '3.3'
|
|
123
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
133
|
requirements:
|
|
125
134
|
- - ">="
|
|
126
135
|
- !ruby/object:Gem::Version
|
|
127
136
|
version: '0'
|
|
128
137
|
requirements: []
|
|
129
|
-
rubygems_version: 3.
|
|
130
|
-
signing_key:
|
|
138
|
+
rubygems_version: 3.6.9
|
|
131
139
|
specification_version: 4
|
|
132
140
|
summary: Container-agnostic automatic constructor injection
|
|
133
141
|
test_files: []
|