rubocop-katalyst 3.0.1 → 3.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a3da31c89d2b9f9926408c588371742a0ae1d8b912c56cb44273616135b528c
4
- data.tar.gz: 308439a55bc2d7c2d40a43f1d5eaf879a53d599a707f1366cf802703cfe62d83
3
+ metadata.gz: 408d4a24b9d76524197c2d2773434353c0a90752578516322fbf78731d13839e
4
+ data.tar.gz: cc011b88f7a8798268c95a79da2ed7c1a7fa0da56b323e11735ff9ca1af22cce
5
5
  SHA512:
6
- metadata.gz: 75bf1de953b0125adab24812789f19fe06dbb96c67f608a824d97fbbdc77185b08a855d18b2d3d36108cccfd53cd264220f34e49f67864f88442854b5cd09623
7
- data.tar.gz: 9c8d94ef1dddc927453f8f3548d4410c92a4d1eb1cd48873ba4b29122031c14e88021d430f830d98a145352ef188b01decb8dd1cae7f6546aefde520cd84b73e
6
+ metadata.gz: 44dd1cab9b0424474fbaf1829d9279d6ae43e6e9a375b6f15f66ada55c5deaa7df66b306c79a2bc6769754797d0bbbd41d6942614c1efe93806e6f96f949f56b
7
+ data.tar.gz: 1cf92f1bf724c885a5bbaf4773c42d2bd9a33bcdd3f3f21edf6c0353953084c286e66976aa946bd5ef61f8753b8fc0eafab8ea9d5cf5873d28e7ebda52228a8c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [3.1.0] - 2026-08-14
2
+
3
+ - Add Koi department for custom cops specific to katalyst-koi projects,
4
+ configured in `config/rubocop-koi.yml` and applied to admin views via erb_lint
5
+ - Add Koi/TableLinkHeading cop: tables in admin index/archived views and row
6
+ partials should link to the record once, from the column that identifies
7
+ it. A single record link must be marked `heading: true` (autocorrectable);
8
+ additional links should use `text`, or `heading: false` when intentional
9
+ - Run tests and linting in CI via `bin/ci`
10
+ - Remove prettier/yarn from this repository and stop shipping `package.json`
11
+ in the gem — nothing consumed it, and downstream `PrettierTask` installs
12
+ prettier in the host project itself
13
+
1
14
  ## [3.0.0] - 2026-05-21
2
15
 
3
16
  - Update Ruby minimum version and syntax to 4.0
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Rubocop::Katalyst
2
2
 
3
- [Katalyst's](https://katalyst.com.au) style guide for Ruby and Rails, in gem form. Katalyst does not at present have any custom cops; this is simply a repository for our configuration of the standard cops with Rails, Rake, Performance and RSpec extensions.
3
+ [Katalyst's](https://katalyst.com.au) style guide for Ruby and Rails, in gem form. This is a repository for our configuration of the standard cops with Rails, Rake, Performance and RSpec extensions, plus our own custom cops.
4
4
 
5
5
  Cops are broken down by department; a file corresponding to each department can be found in the `config` directory, as well as the `default.yml` file which contains any global configuration and loads the cops. The `.rubocop.yml` file contains the configuration for this particular project as a guide for use in non-Rails environments.
6
6
 
7
+ Custom cops in the `Koi` department apply to projects built on [katalyst-koi](https://github.com/katalyst/koi). They target admin views, which plain RuboCop does not inspect — they take effect when RuboCop runs through [erb_lint](https://github.com/Shopify/erb_lint) (`rake erb_lint`), which lints the Ruby inside ERB templates. If your `.erb_lint.yml` restricts the RuboCop linter with an `only:` list, add the `Koi` cops to that list.
8
+
7
9
  ## Installation
8
10
 
9
11
  Add this line to your application's Gemfile:
data/config/default.yml CHANGED
@@ -15,6 +15,7 @@ inherit_mode:
15
15
  inherit_from:
16
16
  - "./rubocop-bundler.yml"
17
17
  - "./rubocop-gemspec.yml"
18
+ - "./rubocop-koi.yml"
18
19
  - "./rubocop-layout.yml"
19
20
  - "./rubocop-lint.yml"
20
21
  - "./rubocop-metrics.yml"
@@ -0,0 +1,17 @@
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
+ Koi:
6
+ Enabled: true
7
+
8
+ Koi/TableLinkHeading:
9
+ Description: "Tables should have a single record link, rendered as the row heading."
10
+ Enabled: true
11
+ Safe: true
12
+ SafeAutoCorrect: false
13
+ VersionAdded: "3.1"
14
+ Include:
15
+ - "**/app/views/admin/**/index.html.erb"
16
+ - "**/app/views/admin/**/archived.html.erb"
17
+ - "**/app/views/admin/**/*.html+row.erb"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "koi/table_link_heading"
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Koi
6
+ # Koi admin tables should link to each record exactly once, from the
7
+ # column that identifies the record, and that link should render as a
8
+ # row heading (`th`) for accessibility.
9
+ #
10
+ # When a table renders a single record link, mark it as the row heading
11
+ # with `heading: true` (autocorrectable). When a table renders more
12
+ # than one record link, the extra columns should use `text` instead;
13
+ # `heading: false` documents an intentional exception. Links that
14
+ # override `url:` are not record links, so they are ignored, as are
15
+ # links that forward options (`**options`).
16
+ #
17
+ # erb_lint feeds RuboCop one ERB tag at a time, so the fragment being
18
+ # inspected is not enough context to count links. When linting a view,
19
+ # this cop reads the whole file from disk and counts record links
20
+ # across all of its ERB tags. Outside a view file it counts links in
21
+ # the current source. Counting is per file, so a view that renders two
22
+ # tables is treated as one.
23
+ #
24
+ # This cop only takes effect through erb_lint: plain `rubocop` does not
25
+ # inspect `.erb` files, and the `Include` configuration restricts it to
26
+ # Koi admin views.
27
+ #
28
+ # @example
29
+ # # bad
30
+ # row.link(:name)
31
+ #
32
+ # # good
33
+ # row.link(:name, heading: true)
34
+ #
35
+ # # good - not a record link
36
+ # row.link(:name, url: :edit_admin_page_path)
37
+ #
38
+ # # good - intentional extra link
39
+ # row.link(:name, heading: true)
40
+ # row.link(:homepage, heading: false)
41
+ class TableLinkHeading < Base
42
+ extend AutoCorrector
43
+
44
+ MSG_HEADING = "The record link should be the row heading; add `heading: true`."
45
+ MSG_EXTRA = "Tables should link to the record once, from the column that identifies it. " \
46
+ "Use `text` instead, or `heading: false` to keep an intentional extra link."
47
+
48
+ RESTRICT_ON_SEND = %i[link].freeze
49
+
50
+ # How erb_lint trims trailing block expressions from ERB tags, copied
51
+ # from Rails: action_view/template/handlers/erb/erubi.rb
52
+ BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
53
+
54
+ ERB_TAG = /<%(?:(?!%>).)*%>/m
55
+
56
+ # @!method row_link?(node)
57
+ def_node_matcher :row_link?, <<~PATTERN
58
+ (send {(send nil? :row) (lvar :row)} :link _ ...)
59
+ PATTERN
60
+
61
+ def on_new_investigation
62
+ super
63
+ @record_link_count = nil
64
+ end
65
+
66
+ def on_send(node)
67
+ return unless row_link?(node)
68
+
69
+ options = options(node)
70
+ return if options && explicit_options?(options)
71
+
72
+ anchor = node.arguments.reject(&:block_pass_type?).last
73
+ return unless anchor
74
+
75
+ if record_link_count > 1
76
+ add_offense(node, message: MSG_EXTRA)
77
+ else
78
+ add_offense(node, message: MSG_HEADING) do |corrector|
79
+ corrector.insert_after(anchor, ", heading: true")
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def options(node)
87
+ arg = node.arguments.reject(&:block_pass_type?).last
88
+ arg if arg&.hash_type?
89
+ end
90
+
91
+ # Skip if heading is already set, options are forwarded (splat), or
92
+ # the link is not a record link (url override).
93
+ def explicit_options?(options)
94
+ options.children.any?(&:kwsplat_type?) ||
95
+ options.pairs.any? { |pair| pair.key.sym_type? && %i[heading url].include?(pair.key.value) }
96
+ end
97
+
98
+ # A `row.link` renders a record link unless it overrides `url:`.
99
+ # Links that forward options are indeterminate, so they don't count.
100
+ def record_link?(node)
101
+ return false unless row_link?(node)
102
+
103
+ options = options(node)
104
+ return true if options.nil?
105
+
106
+ options.children.none?(&:kwsplat_type?) &&
107
+ options.pairs.none? { |pair| pair.key.sym_type? && pair.key.value == :url }
108
+ end
109
+
110
+ def record_link_count
111
+ @record_link_count ||= if (source = view_source)
112
+ count_record_links_in_erb(source)
113
+ else
114
+ count_record_links(processed_source.ast)
115
+ end
116
+ end
117
+
118
+ def view_source
119
+ path = processed_source.file_path
120
+
121
+ File.read(path) if path&.end_with?(".erb") && File.file?(path)
122
+ end
123
+
124
+ def count_record_links_in_erb(source)
125
+ source.scan(ERB_TAG).sum do |tag|
126
+ next 0 if tag.start_with?("<%#")
127
+
128
+ code = tag.sub(/\A<%[=-]*/, "").sub(/-?%>\z/, "").sub(BLOCK_EXPR, "")
129
+ count_record_links(parse_fragment(code))
130
+ end
131
+ end
132
+
133
+ def count_record_links(ast)
134
+ return 0 if ast.nil?
135
+
136
+ ast.each_node(:send).count { |node| record_link?(node) }
137
+ end
138
+
139
+ def parse_fragment(code)
140
+ ProcessedSource.new(code, processed_source.ruby_version).ast
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -6,6 +6,8 @@ require "rubocop-rails"
6
6
  require "rubocop-rake"
7
7
  require "rubocop-rspec"
8
8
 
9
+ require_relative "cop/katalyst_cops"
10
+
9
11
  module RuboCop
10
12
  module Katalyst
11
13
  class Plugin < LintRoller::Plugin
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.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
@@ -148,6 +148,7 @@ files:
148
148
  - config/default.yml
149
149
  - config/rubocop-bundler.yml
150
150
  - config/rubocop-gemspec.yml
151
+ - config/rubocop-koi.yml
151
152
  - config/rubocop-layout.yml
152
153
  - config/rubocop-lint.yml
153
154
  - config/rubocop-metrics.yml
@@ -158,12 +159,13 @@ files:
158
159
  - config/rubocop-rspec.yml
159
160
  - config/rubocop-security.yml
160
161
  - config/rubocop-style.yml
162
+ - lib/rubocop/cop/katalyst_cops.rb
163
+ - lib/rubocop/cop/koi/table_link_heading.rb
161
164
  - lib/rubocop/katalyst.rb
162
165
  - lib/rubocop/katalyst/erb_lint_task.rb
163
166
  - lib/rubocop/katalyst/prettier_task.rb
164
167
  - lib/rubocop/katalyst/rake_task.rb
165
168
  - lib/tasks/erb_lint.rake
166
- - package.json
167
169
  homepage: https://github.com/katalyst/rubocop-katalyst
168
170
  licenses:
169
171
  - MIT
@@ -185,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
187
  - !ruby/object:Gem::Version
186
188
  version: '0'
187
189
  requirements: []
188
- rubygems_version: 4.0.10
190
+ rubygems_version: 4.0.16
189
191
  specification_version: 4
190
192
  summary: Code standards for Katalyst
191
193
  test_files: []
data/package.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "private": true,
3
- "scripts": {
4
- "lint": "prettier --check *.json app/assets/javascripts app/assets/stylesheets",
5
- "autocorrect": "prettier --write *.json app/assets/javascripts app/assets/stylesheets"
6
- },
7
- "devDependencies": {
8
- "prettier": "3.8.3"
9
- }
10
- }