rubocop-hanami 0.2.0 → 0.2.1
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 +26 -1
- data/config/default.yml +1 -0
- data/lib/rubocop/cop/hanami/slice_namespace.rb +37 -4
- data/lib/rubocop/cop/hanami/unused_deps.rb +65 -12
- data/lib/rubocop/hanami/class_index.rb +153 -0
- data/lib/rubocop-hanami.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b66d94028f3350e9dc3966897abca014e9ccad980d431858e664b43421293cd2
|
|
4
|
+
data.tar.gz: 461f52c846ff69d42d92f1ba5e9f88259b88e32607f0f61130e0d650e359e95d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c675f410f6986ad1f8d8cce76f009ed5bdec6b33311d1ce4d49c2f0086d301f17cd4dd1f0d93e23406edad78d90ea5429f08bb50927fc2c358e94cda0a8c9e05
|
|
7
|
+
data.tar.gz: 83e3270654f1717d865a996e5257becb33edc829cb5ce1d8bd0722762586996465683d6d0793c0bb9246f30e0deb9ec21a79171150d1698346e3fd151eba4161
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [v0.2.1] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `AllowedNames` for `Hanami/UnusedDeps`, a list of accessor names the cop never flags in any class. Use it for names
|
|
15
|
+
read somewhere the cop can't follow. It also hides a real unused dependency of that name, so keep the list short.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `Hanami/UnusedDeps` counts a dependency as used when a parent class, a subclass at any depth, or a module either
|
|
20
|
+
one includes reads it. A base operation that reads `contract` while each subclass injects its own no longer flags
|
|
21
|
+
every subclass. The cop parses every file under `app`, `lib` and `slices` to follow them, and stops at constants
|
|
22
|
+
the project doesn't define, such as `Hanami::Action`.
|
|
23
|
+
- While `Hanami/UnusedDeps` is on, any change to a file under `app`, `lib` or `slices` clears RuboCop's cached
|
|
24
|
+
results for every cop and every file, since one class's result now depends on others.
|
|
25
|
+
- `Hanami/SliceNamespace` reads the acronyms in `config/app.rb`, so `inflections.acronym "TMAS"` turns
|
|
26
|
+
`tmas_gate_counts` into `TMASGateCounts` with no cop config. `Inflections` keys now match word by word as well as
|
|
27
|
+
by whole slice name.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `Hanami/UnusedDeps` flagged a dependency that `initialize` took as a keyword and read in its body. A keyword only
|
|
32
|
+
passed on to `super` still counts as unused.
|
|
33
|
+
|
|
10
34
|
## [v0.2.0] - 2026-10-07
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -53,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
53
77
|
`ParentClasses` names the superclasses that mark a class as a slice, `Hanami::Slice` by default.
|
|
54
78
|
`RequireExport: false` lets a slice leave out `export`.
|
|
55
79
|
|
|
56
|
-
[Unreleased]: https://github.com/aaronmallen/rubocop-hanami/compare/0.2.
|
|
80
|
+
[Unreleased]: https://github.com/aaronmallen/rubocop-hanami/compare/0.2.1...HEAD
|
|
81
|
+
[v0.2.1]: https://github.com/aaronmallen/rubocop-hanami/releases/tag/0.2.1
|
|
57
82
|
[v0.2.0]: https://github.com/aaronmallen/rubocop-hanami/releases/tag/0.2.0
|
|
58
83
|
[v0.1.0]: https://github.com/aaronmallen/rubocop-hanami/releases/tag/0.1.0
|
data/config/default.yml
CHANGED
|
@@ -8,9 +8,11 @@ module RuboCop
|
|
|
8
8
|
# `Admin::Actions::Users::Index`. A mismatch raises at load time, often far from the file at
|
|
9
9
|
# fault.
|
|
10
10
|
#
|
|
11
|
-
# The cop camelizes the slice's directory, so `admin_panel`
|
|
12
|
-
# the first segment of each name.
|
|
13
|
-
#
|
|
11
|
+
# The cop camelizes the slice's directory the way Hanami does, word by word, so `admin_panel`
|
|
12
|
+
# gives `AdminPanel`, and checks only the first segment of each name. It reads the acronyms in
|
|
13
|
+
# `config/app.rb`, so `inflections.acronym "TMAS"` turns `tmas_gate_counts` into `TMASGateCounts`.
|
|
14
|
+
# `Inflections` adds to them, keyed by word or by whole slice name. For nested slices, the
|
|
15
|
+
# innermost one counts.
|
|
14
16
|
#
|
|
15
17
|
# @example
|
|
16
18
|
# # slices/admin/actions/users/index.rb
|
|
@@ -37,6 +39,14 @@ module RuboCop
|
|
|
37
39
|
MSG = "Files in `slices/%<slice>s` belong in the `%<namespace>s` namespace."
|
|
38
40
|
SLICE = %r{(?:\A|/)slices/([^/]+)(?=/)}
|
|
39
41
|
|
|
42
|
+
def_node_search :acronyms, "(send _ :acronym $...)"
|
|
43
|
+
|
|
44
|
+
# Lets RuboCop's result cache drop stale results when the app's inflections change.
|
|
45
|
+
def external_dependency_checksum
|
|
46
|
+
path = app_config_path
|
|
47
|
+
Digest::SHA1.hexdigest(File.read(path)) if File.file?(path)
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
def on_new_investigation
|
|
41
51
|
slice = slice_name
|
|
42
52
|
return unless slice && processed_source.ast
|
|
@@ -51,8 +61,31 @@ module RuboCop
|
|
|
51
61
|
|
|
52
62
|
private
|
|
53
63
|
|
|
64
|
+
# Hanami camelizes a slice's name with its parent's inflector. Only the app's is read, so a nested
|
|
65
|
+
# slice ignores acronyms set in its parent slice.
|
|
66
|
+
def app_acronyms
|
|
67
|
+
ast = app_config_ast
|
|
68
|
+
return {} unless ast
|
|
69
|
+
|
|
70
|
+
words = acronyms(ast).to_a.flatten.select { |arg| arg.type?(:str, :sym) }
|
|
71
|
+
words.to_h { |word| [word.value.to_s.downcase, word.value.to_s] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def app_config_ast
|
|
75
|
+
path = app_config_path
|
|
76
|
+
return unless File.file?(path)
|
|
77
|
+
|
|
78
|
+
source = processed_source
|
|
79
|
+
ProcessedSource.from_file(path, source.ruby_version, parser_engine: source.parser_engine).ast
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def app_config_path
|
|
83
|
+
File.join(config.base_dir_for_path_parameters, "config", "app.rb")
|
|
84
|
+
end
|
|
85
|
+
|
|
54
86
|
def namespace_for(slice)
|
|
55
|
-
(cop_config["Inflections"] || {})
|
|
87
|
+
rules = app_acronyms.merge(cop_config["Inflections"] || {})
|
|
88
|
+
rules.fetch(slice) { slice.split("_").map { |word| rules.fetch(word) { word.capitalize } }.join }.to_s
|
|
56
89
|
end
|
|
57
90
|
|
|
58
91
|
def root_name(const)
|
|
@@ -7,13 +7,19 @@ module RuboCop
|
|
|
7
7
|
# dependency still gets resolved, still boots its provider and still shows up as a constructor
|
|
8
8
|
# argument.
|
|
9
9
|
#
|
|
10
|
-
# A read is a bare call or a call on `self`, the instance variable the kwargs strategy sets,
|
|
11
|
-
# `send`, `public_send`, `__send__` or `method` with a literal symbol
|
|
10
|
+
# A read is a bare call or a call on `self`, the instance variable the kwargs strategy sets,
|
|
11
|
+
# `send`, `public_send`, `__send__` or `method` with a literal symbol, or a keyword argument
|
|
12
|
+
# of `initialize` read outside `super`. The cop skips a class
|
|
12
13
|
# that passes `Deps` anything but literal keys, or calls `send` with a name it can't read. It
|
|
13
14
|
# skips modules, since `include Deps` in a module serves whatever class includes it.
|
|
14
15
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# A dependency also counts as used when a parent class, a subclass at any depth, or a mixin of
|
|
17
|
+
# either reads it. The cop indexes the classes and modules under `app`, `lib` and `slices` to
|
|
18
|
+
# follow them, and stops at constants the project doesn't define, such as `Hanami::Action`. It
|
|
19
|
+
# skips the class if one of them calls `send` with a name it can't read.
|
|
20
|
+
#
|
|
21
|
+
# It still flags a dependency read somewhere it can't follow. Disable it with a comment on that
|
|
22
|
+
# key, or name it in `AllowedNames` to never flag it.
|
|
17
23
|
#
|
|
18
24
|
# @example
|
|
19
25
|
# # bad
|
|
@@ -33,23 +39,38 @@ module RuboCop
|
|
|
33
39
|
# user_repo.create(input)
|
|
34
40
|
# end
|
|
35
41
|
# end
|
|
42
|
+
#
|
|
43
|
+
# @example AllowedNames: [contract]
|
|
44
|
+
# # good
|
|
45
|
+
# class Operations::SaveTask < MyApp::Operation
|
|
46
|
+
# include Deps[contract: "contracts.task"]
|
|
47
|
+
# end
|
|
36
48
|
class UnusedDeps < Base
|
|
37
49
|
include DepsKeys
|
|
38
50
|
|
|
39
51
|
MSG = "`%<name>s` from `Deps[\"%<key>s\"]` is never used."
|
|
40
52
|
|
|
41
53
|
def_node_matcher :dynamic_send?, "(send _ {:send :public_send :__send__} !sym ...)"
|
|
54
|
+
def_node_search :initializers, "(def :initialize ...)"
|
|
42
55
|
def_node_matcher :literal_send, "(send _ {:send :public_send :__send__ :method} (sym $_) ...)"
|
|
43
56
|
|
|
57
|
+
# Lets RuboCop's result cache drop stale results when a parent class or mixin changes.
|
|
58
|
+
def external_dependency_checksum
|
|
59
|
+
RuboCop::Hanami::ClassIndex.checksum(config.base_dir_for_path_parameters)
|
|
60
|
+
end
|
|
61
|
+
|
|
44
62
|
def on_class(node)
|
|
45
63
|
arguments = deps_arguments_in(node)
|
|
46
64
|
return unless checkable?(node, arguments)
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
unused = unused_keys(arguments, used_names(node) + Array(cop_config["AllowedNames"]))
|
|
67
|
+
return if unused.empty?
|
|
68
|
+
|
|
69
|
+
related = related_reads(node)
|
|
70
|
+
return unless related
|
|
51
71
|
|
|
52
|
-
|
|
72
|
+
unused.each do |key_node, name|
|
|
73
|
+
add_offense(key_node, message: format(MSG, name: name, key: key_node.value)) unless related.include?(name)
|
|
53
74
|
end
|
|
54
75
|
end
|
|
55
76
|
|
|
@@ -61,22 +82,54 @@ module RuboCop
|
|
|
61
82
|
node.each_descendant(:send).none? { |send| dynamic_send?(send) }
|
|
62
83
|
end
|
|
63
84
|
|
|
85
|
+
def class_index
|
|
86
|
+
RuboCop::Hanami::ClassIndex.for(config.base_dir_for_path_parameters, processed_source) do |node|
|
|
87
|
+
used_names(node) if node.each_descendant(:send).none? { |send| dynamic_send?(send) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
def deps_arguments_in(node)
|
|
65
92
|
body = node.body
|
|
66
93
|
children = body&.begin_type? ? body.children : [body]
|
|
67
94
|
children.compact.filter_map { |child| deps_arguments(child) }.flatten
|
|
68
95
|
end
|
|
69
96
|
|
|
97
|
+
# Passing a keyword on to `super` doesn't count, since the kwargs strategy needs that to inject it.
|
|
98
|
+
def keyword_reads(method)
|
|
99
|
+
keywords = method.arguments.select { |argument| argument.type?(:kwarg, :kwoptarg) }.map(&:name)
|
|
100
|
+
reads = method.each_descendant(:lvar).select { |lvar| lvar.each_ancestor(:super).none? }.map(&:name)
|
|
101
|
+
(keywords & reads).map(&:to_s)
|
|
102
|
+
end
|
|
103
|
+
|
|
70
104
|
def literal?(argument)
|
|
71
105
|
argument.str_type? || (argument.hash_type? && argument.pairs.all? { |pair| literal_alias?(pair) })
|
|
72
106
|
end
|
|
73
107
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
next read.children.first.to_s.delete_prefix("@") if read.ivar_type?
|
|
108
|
+
def read_name(read)
|
|
109
|
+
return read.children.first.to_s.delete_prefix("@") if read.ivar_type?
|
|
77
110
|
|
|
78
|
-
|
|
111
|
+
literal_send(read)&.to_s || (read.method_name.to_s if read.receiver.nil? || read.receiver.self_type?)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The names every parent, child and mixin of `node` reads, or nil when one of them reads names
|
|
115
|
+
# the cop can't see.
|
|
116
|
+
def related_reads(node)
|
|
117
|
+
index = class_index
|
|
118
|
+
reads = index.related(index.definition(node, nil)).map(&:reads)
|
|
119
|
+
reads.sum(Set[]) if reads.all?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def unused_keys(arguments, used)
|
|
123
|
+
unused = []
|
|
124
|
+
each_deps_key(arguments) do |key_node, name|
|
|
125
|
+
unused << [key_node, name] unless name.nil? || used.include?(name)
|
|
79
126
|
end
|
|
127
|
+
unused
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def used_names(node)
|
|
131
|
+
reads = node.each_descendant(:send, :ivar).to_set { |read| read_name(read) }
|
|
132
|
+
reads.merge(initializers(node).flat_map { |method| keyword_reads(method) })
|
|
80
133
|
end
|
|
81
134
|
end
|
|
82
135
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Hanami
|
|
5
|
+
# Indexes the classes and modules defined under a project's `app`, `lib` and `slices`, so a cop can
|
|
6
|
+
# follow a class to the parents and mixins it inherits from, and to the classes that inherit from
|
|
7
|
+
# it, across files.
|
|
8
|
+
class ClassIndex
|
|
9
|
+
extend NodePattern::Macros
|
|
10
|
+
|
|
11
|
+
GLOB = "{app,lib,slices}/**/*.rb"
|
|
12
|
+
# ponytail: rescans at most once a second, so a language server may lint against a one second old
|
|
13
|
+
# project. Watch files instead if that ever matters.
|
|
14
|
+
TTL = 1
|
|
15
|
+
|
|
16
|
+
# `parents` holds each superclass or mixin as written, with the lexical scopes to resolve it in.
|
|
17
|
+
# `reads` is the set of names the body reads, or nil when it reads names the cop can't see.
|
|
18
|
+
Definition = Struct.new(:name, :parents, :reads)
|
|
19
|
+
|
|
20
|
+
def_node_matcher :mixin_arguments, "(send nil? {:include :prepend} $...)"
|
|
21
|
+
|
|
22
|
+
@indexes = {}
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Changes whenever a file under the indexed roots is added, removed or touched.
|
|
26
|
+
def checksum(root)
|
|
27
|
+
Digest::SHA1.hexdigest(Dir.glob(GLOB, base: root).sort.map { |path| "#{path}#{mtime(root, path)}" }.join)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# One index per project root, shared by every file in the run. `reads` takes a class or module
|
|
31
|
+
# node and returns its Definition#reads.
|
|
32
|
+
def for(root, processed_source, &reads)
|
|
33
|
+
index = @indexes[root] ||= new(root, processed_source, reads)
|
|
34
|
+
index.refresh
|
|
35
|
+
index
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mtime(root, path)
|
|
39
|
+
File.mtime(File.join(root, path)).to_f
|
|
40
|
+
rescue Errno::ENOENT
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The constant's full name, as Module#name would give it.
|
|
45
|
+
def name_of(node)
|
|
46
|
+
name = node.identifier.source
|
|
47
|
+
return name.delete_prefix("::") if name.start_with?("::")
|
|
48
|
+
|
|
49
|
+
outer = node.each_ancestor(:class, :module).first
|
|
50
|
+
outer ? "#{name_of(outer)}::#{name}" : name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The full names of the classes and modules around `node`, innermost first.
|
|
54
|
+
def scopes_of(node)
|
|
55
|
+
node.each_ancestor(:class, :module).map { |scope| name_of(scope) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(root, processed_source, reads)
|
|
60
|
+
@root = root
|
|
61
|
+
@ruby_version = processed_source.ruby_version
|
|
62
|
+
@parser_engine = processed_source.parser_engine
|
|
63
|
+
@reads = reads
|
|
64
|
+
@files = {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Every definition `definition` inherits from, through superclasses and mixins, that the project
|
|
68
|
+
# defines. A constant from outside the project, such as `Hanami::Action`, ends the walk.
|
|
69
|
+
def ancestors(definition, seen = Set[definition.name])
|
|
70
|
+
definition.parents.flat_map do |constant, scopes|
|
|
71
|
+
name = resolve(constant, scopes, definition.name)
|
|
72
|
+
next [] unless name && seen.add?(name)
|
|
73
|
+
|
|
74
|
+
@definitions[name].flat_map { |parent| [parent, *ancestors(parent, seen)] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Every definition that inherits from `name`, at any depth.
|
|
79
|
+
def descendants(name, seen = Set[name])
|
|
80
|
+
@children.fetch(name, []).flat_map do |child|
|
|
81
|
+
seen.add?(child.name) ? [child, *descendants(child.name, seen)] : []
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def definition(node, reads = @reads.call(node))
|
|
86
|
+
name = self.class.name_of(node)
|
|
87
|
+
scopes = self.class.scopes_of(node)
|
|
88
|
+
parents = mixins(node).map { |constant| [constant.source, [name, *scopes]] }
|
|
89
|
+
parents.unshift([node.parent_class.source, scopes]) if node.class_type? && node.parent_class&.const_type?
|
|
90
|
+
Definition.new(name, parents, reads)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Parses only the files added or touched since the last scan.
|
|
94
|
+
def refresh
|
|
95
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
96
|
+
return if @checked_at && now - @checked_at < TTL
|
|
97
|
+
|
|
98
|
+
@checked_at = now
|
|
99
|
+
@files = Dir.glob(GLOB, base: @root).to_h { |path| [path, file_entry(path)] }
|
|
100
|
+
definitions = @files.values.flat_map(&:last)
|
|
101
|
+
@definitions = definitions.group_by(&:name)
|
|
102
|
+
@children = children_of(definitions)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Everything whose reads count for `definition`: its ancestors, its descendants, and the ancestors
|
|
106
|
+
# of those descendants, such as a mixin a subclass includes.
|
|
107
|
+
def related(definition)
|
|
108
|
+
family = [definition, *descendants(definition.name)]
|
|
109
|
+
(family.drop(1) + family.flat_map { |member| ancestors(member) }).uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def children_of(definitions)
|
|
115
|
+
children = Hash.new { |hash, name| hash[name] = [] }
|
|
116
|
+
definitions.each do |child|
|
|
117
|
+
child.parents.each do |constant, scopes|
|
|
118
|
+
name = resolve(constant, scopes, child.name)
|
|
119
|
+
children[name] << child if name
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
children
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def file_entry(path)
|
|
126
|
+
mtime = self.class.mtime(@root, path)
|
|
127
|
+
cached = @files[path]
|
|
128
|
+
cached&.first == mtime ? cached : [mtime, parse(path)]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def mixins(node)
|
|
132
|
+
body = node.body
|
|
133
|
+
children = body&.begin_type? ? body.children : [body]
|
|
134
|
+
children.compact.flat_map { |child| mixin_arguments(child) || [] }.select(&:const_type?)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse(path)
|
|
138
|
+
ast = ProcessedSource.from_file(File.join(@root, path), @ruby_version, parser_engine: @parser_engine).ast
|
|
139
|
+
ast ? ast.each_node(:class, :module).map { |node| definition(node) } : []
|
|
140
|
+
rescue Errno::ENOENT
|
|
141
|
+
[]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Looks the constant up the way Ruby would, from the innermost scope out, skipping the class that
|
|
145
|
+
# names it so `class Operation < Operation` finds the outer one.
|
|
146
|
+
def resolve(constant, scopes, skip)
|
|
147
|
+
candidates = [*scopes.map { |scope| "#{scope}::#{constant}" }, constant]
|
|
148
|
+
candidates = [constant.delete_prefix("::")] if constant.start_with?("::")
|
|
149
|
+
candidates.find { |name| name != skip && @definitions.key?(name) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/rubocop-hanami.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-hanami
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aaron Allen
|
|
@@ -61,6 +61,7 @@ files:
|
|
|
61
61
|
- lib/rubocop/cop/hanami/unused_deps.rb
|
|
62
62
|
- lib/rubocop/cop/hanami/unvalidated_params.rb
|
|
63
63
|
- lib/rubocop/cop/hanami/unwrapped_step.rb
|
|
64
|
+
- lib/rubocop/hanami/class_index.rb
|
|
64
65
|
- lib/rubocop/hanami/plugin.rb
|
|
65
66
|
homepage: https://github.com/aaronmallen/rubocop-hanami
|
|
66
67
|
licenses:
|