rubocop-katalyst 3.1.0 → 3.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 +6 -0
- data/config/rubocop-koi.yml +13 -3
- data/lib/rubocop/cop/katalyst_cops.rb +1 -0
- data/lib/rubocop/cop/koi/duplicates_association.rb +117 -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: ca026117c849f4df349fe5c67aed0695aedfc780a64af122996eb3d51fc3cc28
|
|
4
|
+
data.tar.gz: db9a73d62f9d1f0005540ae233de188ce4fa7486272a06387ac72335b8bc3037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 752ce4174f0ea3121ba76a2cfe72c49771e0069dc4f940eda114f789ed13a73cc1dbe88558efb4a193765c3b5038003a8af8a2c3979a4b126d515d5b171304c2
|
|
7
|
+
data.tar.gz: d9dd3b37ec0884d11b0d08a0ceb228315174b00ffdc6736246bd1da5e9f8271f7fa1d2af91809170d5ebfc2d0f664daba801d0d8b0ac5be4ed7d46b8150859c7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [3.2.0] - 2026-08-17
|
|
2
|
+
|
|
3
|
+
- Add Koi/DuplicatesAssociation identifying Katalyst::Content items that
|
|
4
|
+
have associations but don't use the library-provided `duplicates_association`
|
|
5
|
+
helper.
|
|
6
|
+
|
|
1
7
|
## [3.1.0] - 2026-08-14
|
|
2
8
|
|
|
3
9
|
- Add Koi department for custom cops specific to katalyst-koi projects,
|
data/config/rubocop-koi.yml
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# Custom cops for projects built on Koi (katalyst-koi).
|
|
2
|
-
#
|
|
3
|
-
# These cops target admin views, which are only linted when RuboCop runs
|
|
4
|
-
# through erb_lint, so they take effect via `rake erb_lint`.
|
|
5
2
|
Koi:
|
|
6
3
|
Enabled: true
|
|
7
4
|
|
|
5
|
+
Koi/DuplicatesAssociation:
|
|
6
|
+
Description: "Owned detail associations on content items should declare duplicates_association."
|
|
7
|
+
Enabled: true
|
|
8
|
+
Safe: true
|
|
9
|
+
SafeAutoCorrect: false
|
|
10
|
+
VersionAdded: "3.2"
|
|
11
|
+
BaseClasses:
|
|
12
|
+
- Katalyst::Content::Item
|
|
13
|
+
Include:
|
|
14
|
+
- "**/app/models/**/*.rb"
|
|
15
|
+
|
|
16
|
+
# Targets admin views, which are only linted when RuboCop runs through
|
|
17
|
+
# erb_lint, so it takes effect via `rake erb_lint`.
|
|
8
18
|
Koi/TableLinkHeading:
|
|
9
19
|
Description: "Tables should have a single record link, rendered as the row heading."
|
|
10
20
|
Enabled: true
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Koi
|
|
6
|
+
# Content items are duplicated for copy-on-write editing, and owned
|
|
7
|
+
# detail records must be duplicated with them. katalyst-content
|
|
8
|
+
# provides `duplicates_association` for this; without it the duplicate
|
|
9
|
+
# silently loses the detail record when the item is next edited.
|
|
10
|
+
#
|
|
11
|
+
# An association is considered an owned detail when it declares
|
|
12
|
+
# `dependent: :destroy` and is autosaved, either explicitly with
|
|
13
|
+
# `autosave: true` or implicitly via `accepts_nested_attributes_for`.
|
|
14
|
+
#
|
|
15
|
+
# The cop only applies where `duplicates_association` is available:
|
|
16
|
+
# classes that extend one of the configured `BaseClasses`
|
|
17
|
+
# (`Katalyst::Content::Item` by default), include
|
|
18
|
+
# `Katalyst::Content::DuplicatesAssociations`, or already call
|
|
19
|
+
# `duplicates_association`.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# # bad
|
|
23
|
+
# class Embed < Katalyst::Content::Item
|
|
24
|
+
# has_one :embed_detail, autosave: true, dependent: :destroy
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# class Embed < Katalyst::Content::Item
|
|
29
|
+
# has_one :embed_detail, autosave: true, dependent: :destroy
|
|
30
|
+
# duplicates_association :embed_detail
|
|
31
|
+
# end
|
|
32
|
+
class DuplicatesAssociation < Base
|
|
33
|
+
extend AutoCorrector
|
|
34
|
+
|
|
35
|
+
MSG = "Owned associations are duplicated with the item; " \
|
|
36
|
+
"declare `duplicates_association :%<name>s`."
|
|
37
|
+
|
|
38
|
+
RESTRICT_ON_SEND = %i[has_one has_many].freeze
|
|
39
|
+
|
|
40
|
+
BASE_CLASSES_DEFAULT = ["Katalyst::Content::Item"].freeze
|
|
41
|
+
|
|
42
|
+
# @!method association(node)
|
|
43
|
+
def_node_matcher :association, <<~PATTERN
|
|
44
|
+
(send nil? {:has_one :has_many} (sym $_) $(hash ...))
|
|
45
|
+
PATTERN
|
|
46
|
+
|
|
47
|
+
# @!method duplicates_association_names(node)
|
|
48
|
+
def_node_search :duplicates_association_names, <<~PATTERN
|
|
49
|
+
(send nil? :duplicates_association (sym $_)+)
|
|
50
|
+
PATTERN
|
|
51
|
+
|
|
52
|
+
# @!method includes_concern?(node)
|
|
53
|
+
def_node_search :includes_concern?, <<~PATTERN
|
|
54
|
+
(send nil? :include (const _ :DuplicatesAssociations))
|
|
55
|
+
PATTERN
|
|
56
|
+
|
|
57
|
+
# @!method nested_attributes(node)
|
|
58
|
+
def_node_search :nested_attributes, <<~PATTERN
|
|
59
|
+
$(send nil? :accepts_nested_attributes_for (sym $_) ...)
|
|
60
|
+
PATTERN
|
|
61
|
+
|
|
62
|
+
def on_send(node)
|
|
63
|
+
association(node) do |name, options|
|
|
64
|
+
klass = node.each_ancestor(:class).first
|
|
65
|
+
return unless klass
|
|
66
|
+
return unless owned?(klass, name, options)
|
|
67
|
+
return unless duplication_available?(klass)
|
|
68
|
+
return if declared?(klass, name)
|
|
69
|
+
|
|
70
|
+
add_offense(node, message: format(MSG, name:)) do |corrector|
|
|
71
|
+
anchor = nested_attributes_node(klass, name) || node
|
|
72
|
+
corrector.insert_after(anchor, "\n#{' ' * anchor.loc.column}duplicates_association :#{name}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def owned?(klass, name, options)
|
|
80
|
+
option?(options, :dependent, :destroy) &&
|
|
81
|
+
(option?(options, :autosave, true) || nested_attributes_node(klass, name))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def option?(options, key, value)
|
|
85
|
+
options.pairs.any? do |pair|
|
|
86
|
+
pair.key.sym_type? && pair.key.value == key && option_value?(pair.value, value)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def option_value?(node, value)
|
|
91
|
+
case value
|
|
92
|
+
when true then node.true_type?
|
|
93
|
+
when Symbol then node.sym_type? && node.value == value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def duplication_available?(klass)
|
|
98
|
+
base_classes.include?(klass.parent_class&.source) ||
|
|
99
|
+
includes_concern?(klass) ||
|
|
100
|
+
duplicates_association_names(klass).any?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def base_classes
|
|
104
|
+
cop_config.fetch("BaseClasses", BASE_CLASSES_DEFAULT)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def declared?(klass, name)
|
|
108
|
+
duplicates_association_names(klass).any? { |names| names.include?(name) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def nested_attributes_node(klass, name)
|
|
112
|
+
nested_attributes(klass).find { |_node, sym| sym == name }&.first
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-katalyst
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katalyst Interactive
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- config/rubocop-security.yml
|
|
161
161
|
- config/rubocop-style.yml
|
|
162
162
|
- lib/rubocop/cop/katalyst_cops.rb
|
|
163
|
+
- lib/rubocop/cop/koi/duplicates_association.rb
|
|
163
164
|
- lib/rubocop/cop/koi/table_link_heading.rb
|
|
164
165
|
- lib/rubocop/katalyst.rb
|
|
165
166
|
- lib/rubocop/katalyst/erb_lint_task.rb
|