rubocop-dev_doc 0.16.0 → 0.17.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11175a594b0adb68ca20c3e2ff0ef414d1136d55152cd010a8d95ee26601a759
|
|
4
|
+
data.tar.gz: 460f211f62e554bead8cd267a4c3e918159eccff748d2d03f0f06b56454d025e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b5a9ae0aed54ff1979d4deb8313ef53e0e8b3d831e9d3bbdabaf9c4d9c3897acd7486054cbd012222453c9e6dbcd0054821ea83ddac71c1231bc51d7be6072e
|
|
7
|
+
data.tar.gz: e6acacc3e7f65769104c90d5043a08c6385983ab6c9f38db8088ff30193044ac1cd6e14b365c1cbf521de52d445b84e9c4312f75e2dcb6ab93370077a8134bed
|
data/config/default.yml
CHANGED
|
@@ -403,6 +403,18 @@ DevDoc/Rails/ApplicationRecordTransaction:
|
|
|
403
403
|
Exclude:
|
|
404
404
|
- "app/models/**/*.rb"
|
|
405
405
|
|
|
406
|
+
DevDoc/Rails/ApplicationModelBase:
|
|
407
|
+
Description: "Inherit the app's ApplicationModel root instead of including ActiveModel::Model directly (three-way rule, backend/03_model.md item 9)."
|
|
408
|
+
# Disabled by default: requires the project to have an ApplicationModel root
|
|
409
|
+
# over glib-web's Glib::Model. Enable per project, scoped to app/models; the
|
|
410
|
+
# root itself and any engine-side base need excluding where they legitimately
|
|
411
|
+
# carry the include.
|
|
412
|
+
Enabled: false
|
|
413
|
+
Include:
|
|
414
|
+
- "app/models/**/*.rb"
|
|
415
|
+
Exclude:
|
|
416
|
+
- "app/models/application_model.rb"
|
|
417
|
+
|
|
406
418
|
DevDoc/Style/AvoidOptionsHash:
|
|
407
419
|
Description: "Use keyword arguments instead of `**options` — typos raise `ArgumentError`; options hashes swallow them silently."
|
|
408
420
|
Enabled: true
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Test
|
|
5
|
+
module Lints
|
|
6
|
+
# Runtime check: every CLASS defined by a file under `app/models/`
|
|
7
|
+
# (excluding `concerns/`) must descend from one of the project's domain
|
|
8
|
+
# base classes. Runs with the app loaded, so real ancestry is checked —
|
|
9
|
+
# intermediate family bases and indirect inheritance resolve correctly,
|
|
10
|
+
# which is exactly what a per-file static cop cannot do.
|
|
11
|
+
#
|
|
12
|
+
# Wrapped by the Minitest module `DomainClassBase` below — see that
|
|
13
|
+
# module for the rationale.
|
|
14
|
+
class DomainClassBaseChecker
|
|
15
|
+
# The three-way rule (best_practices/backend/en/03_model.md item 9):
|
|
16
|
+
# persisted -> ApplicationRecord; table-less but form-backed ->
|
|
17
|
+
# ApplicationModel; plain domain logic -> PlainModel.
|
|
18
|
+
DEFAULT_BASE_CLASS_NAMES = %w[ApplicationRecord ApplicationModel PlainModel].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(project_root, base_class_names: DEFAULT_BASE_CLASS_NAMES, allowed_paths: [])
|
|
21
|
+
@project_root = Pathname(project_root)
|
|
22
|
+
@base_class_names = base_class_names
|
|
23
|
+
@allowed_paths = allowed_paths
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns an Array<String> of offender descriptions, or `[]` when
|
|
27
|
+
# every model class descends from an allowed base. Modules are skipped:
|
|
28
|
+
# the three-way rule classifies CLASSES — namespaces, function-bag
|
|
29
|
+
# modules, and concerns carry no instance state to classify.
|
|
30
|
+
def offenders
|
|
31
|
+
bases = @base_class_names.map(&:constantize)
|
|
32
|
+
|
|
33
|
+
model_files.filter_map do |path|
|
|
34
|
+
constant = constant_for(path)
|
|
35
|
+
next if constant.nil? # module or namespace-only file
|
|
36
|
+
next if bases.any? { |base| constant <= base }
|
|
37
|
+
|
|
38
|
+
" #{relative(path)}: #{constant.name} < #{constant.superclass.name} — #{hint_for(constant)}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def model_files
|
|
45
|
+
Dir.glob(@project_root.join('app/models/**/*.rb')).reject do |path|
|
|
46
|
+
relative = relative(path)
|
|
47
|
+
relative.start_with?('app/models/concerns/') ||
|
|
48
|
+
@allowed_paths.any? { |allowed| relative.start_with?(allowed) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Zeitwerk guarantees the file defines the constant its path names, so
|
|
53
|
+
# deriving it from the path (rather than parsing the source) is exact,
|
|
54
|
+
# and nested helper classes (e.g. error classes inside a model) are
|
|
55
|
+
# never enumerated.
|
|
56
|
+
def constant_for(path)
|
|
57
|
+
name = relative(path).delete_prefix('app/models/').delete_suffix('.rb').camelize
|
|
58
|
+
constant = name.constantize
|
|
59
|
+
constant.is_a?(Class) ? constant : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def hint_for(constant)
|
|
63
|
+
if constant.include?(ActiveModel::Model)
|
|
64
|
+
'it includes ActiveModel::Model, so inherit ApplicationModel instead'
|
|
65
|
+
else
|
|
66
|
+
'inherit PlainModel (or ApplicationRecord/ApplicationModel if it persists or backs a form)'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def relative(path)
|
|
71
|
+
Pathname(path).relative_path_from(@project_root).to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Domain-class base tripwire: every class under `app/models/` must be one
|
|
76
|
+
# of the project's three kinds — persisted (ApplicationRecord),
|
|
77
|
+
# form-backed (ApplicationModel), or plain domain logic (PlainModel).
|
|
78
|
+
#
|
|
79
|
+
# ## Rationale
|
|
80
|
+
# The three-way rule (backend/03_model.md item 9) makes the author
|
|
81
|
+
# classify each domain class at creation time; the base's docstring then
|
|
82
|
+
# states the contract that kind carries. Static analysis cannot enforce
|
|
83
|
+
# totality — RuboCop sees one file at a time, so it cannot resolve
|
|
84
|
+
# whether `class Foo < SomeFamilyBase` ultimately reaches an allowed
|
|
85
|
+
# base. This lint checks real ancestry with the app loaded, so
|
|
86
|
+
# intermediate bases resolve and the rule is enforced literally.
|
|
87
|
+
# `Rails/ApplicationRecord` and `DevDoc/Rails/ApplicationModelBase`
|
|
88
|
+
# remain useful beside it for editor-time feedback on the two common
|
|
89
|
+
# direct mistakes.
|
|
90
|
+
#
|
|
91
|
+
# NOTE: Limitations:
|
|
92
|
+
# - Only classes whose files live under `app/models/` are checked; a
|
|
93
|
+
# domain class parked elsewhere is invisible (placement itself is the
|
|
94
|
+
# orchestration taxonomy's reviewer-owned residual).
|
|
95
|
+
# - Modules are skipped by design (namespaces, function-bag modules,
|
|
96
|
+
# concerns) — the rule classifies classes.
|
|
97
|
+
#
|
|
98
|
+
# ## Usage
|
|
99
|
+
# Include this module in a Minitest test class (Rails test env) in a
|
|
100
|
+
# project whose three bases exist. Override the constants on the test
|
|
101
|
+
# class to rename bases or exempt a sanctioned file:
|
|
102
|
+
#
|
|
103
|
+
# class DomainClassBaseTest < ActiveSupport::TestCase
|
|
104
|
+
# include DevDoc::Test::Lints::DomainClassBase
|
|
105
|
+
# # DOMAIN_BASE_CLASS_NAMES = %w[ApplicationRecord ApplicationModel PlainModel].freeze
|
|
106
|
+
# # ALLOWED_DOMAIN_CLASS_PATHS = %w[app/models/legacy/].freeze
|
|
107
|
+
# end
|
|
108
|
+
module DomainClassBase
|
|
109
|
+
# Defaults. Per-project override: redefine the constants on the test
|
|
110
|
+
# class that includes this module.
|
|
111
|
+
DOMAIN_BASE_CLASS_NAMES = DomainClassBaseChecker::DEFAULT_BASE_CLASS_NAMES
|
|
112
|
+
ALLOWED_DOMAIN_CLASS_PATHS = [].freeze
|
|
113
|
+
|
|
114
|
+
def test_every_model_class_descends_from_a_domain_base
|
|
115
|
+
offenders = DomainClassBaseChecker.new(
|
|
116
|
+
Rails.root,
|
|
117
|
+
base_class_names: self.class::DOMAIN_BASE_CLASS_NAMES,
|
|
118
|
+
allowed_paths: self.class::ALLOWED_DOMAIN_CLASS_PATHS
|
|
119
|
+
).offenders
|
|
120
|
+
|
|
121
|
+
assert offenders.empty?, domain_class_base_message(offenders)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def domain_class_base_message(offenders)
|
|
127
|
+
"Classes under app/models must descend from one of " \
|
|
128
|
+
"#{self.class::DOMAIN_BASE_CLASS_NAMES.join(' / ')} — the three-way rule " \
|
|
129
|
+
"(backend/03_model.md item 9): persisted -> ApplicationRecord, form-backed " \
|
|
130
|
+
"-> ApplicationModel, plain domain logic -> PlainModel. If a class is a " \
|
|
131
|
+
"sanctioned exception, add its path to ALLOWED_DOMAIN_CLASS_PATHS on the " \
|
|
132
|
+
"including test class with a comment.\n\n" \
|
|
133
|
+
"Offenders:\n#{offenders.join("\n")}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module DevDoc
|
|
4
|
+
module Rails
|
|
5
|
+
# Inherit the app's `ApplicationModel` root instead of including
|
|
6
|
+
# `ActiveModel::Model` directly in a class under `app/models`.
|
|
7
|
+
#
|
|
8
|
+
# ## Rationale
|
|
9
|
+
# Table-less form-backed domain objects follow the three-way rule
|
|
10
|
+
# (backend/03_model.md item 9): persisted classes inherit
|
|
11
|
+
# `ApplicationRecord`, form-backed POROs inherit `ApplicationModel`
|
|
12
|
+
# (a thin app root over `Glib::Model`, which carries the shared
|
|
13
|
+
# mechanics such as `attr_id_list`), and plain domain logic inherits
|
|
14
|
+
# `PlainModel`. A direct `include ActiveModel::Model` bypasses the
|
|
15
|
+
# shared root: the class silently misses the shared mechanics, and the
|
|
16
|
+
# author never makes the which-kind-is-this decision the rule exists
|
|
17
|
+
# to force. This is the ActiveModel analog of `Rails/ApplicationRecord`.
|
|
18
|
+
#
|
|
19
|
+
# This cop gives editor-time feedback on the single most common direct
|
|
20
|
+
# mistake; totality (every model class descends from one of the three
|
|
21
|
+
# bases, through any intermediate family base) is enforced at test time
|
|
22
|
+
# by `DevDoc::Test::Lints::DomainClassBase`, since resolving indirect
|
|
23
|
+
# ancestry is beyond per-file static analysis.
|
|
24
|
+
#
|
|
25
|
+
# Disabled by default: enable it in projects whose `ApplicationModel`
|
|
26
|
+
# root exists (it requires glib-web's `Glib::Model`).
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# # bad
|
|
30
|
+
# class BulkOperation
|
|
31
|
+
# include ActiveModel::Model
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# class BulkOperation < ApplicationModel
|
|
36
|
+
# end
|
|
37
|
+
class ApplicationModelBase < Base
|
|
38
|
+
MSG = 'Inherit `ApplicationModel` instead of including `ActiveModel::Model` directly — ' \
|
|
39
|
+
'the shared root carries the form-object mechanics (backend/03_model.md item 9).'.freeze
|
|
40
|
+
|
|
41
|
+
RESTRICT_ON_SEND = %i[include].freeze
|
|
42
|
+
|
|
43
|
+
def_node_matcher :active_model_include?, <<~PATTERN
|
|
44
|
+
(send nil? :include (const (const {nil? cbase} :ActiveModel) :Model))
|
|
45
|
+
PATTERN
|
|
46
|
+
|
|
47
|
+
def on_send(node)
|
|
48
|
+
return unless active_model_include?(node)
|
|
49
|
+
|
|
50
|
+
add_offense(node)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-dev_doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dev-doc contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- lib/dev_doc/test/lints/cron_schedule.rb
|
|
96
96
|
- lib/dev_doc/test/lints/cross_tenant_canary_check.rb
|
|
97
97
|
- lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb
|
|
98
|
+
- lib/dev_doc/test/lints/domain_class_base.rb
|
|
98
99
|
- lib/dev_doc/test/lints/duplicate_snapshot.rb
|
|
99
100
|
- lib/dev_doc/test/lints/enqueue_disable_naming.rb
|
|
100
101
|
- lib/dev_doc/test/lints/external_io_boundary.rb
|
|
@@ -133,6 +134,7 @@ files:
|
|
|
133
134
|
- lib/rubocop/cop/dev_doc/migration/require_primary_key.rb
|
|
134
135
|
- lib/rubocop/cop/dev_doc/migration/require_reference_foreign_key.rb
|
|
135
136
|
- lib/rubocop/cop/dev_doc/migration/require_timestamps.rb
|
|
137
|
+
- lib/rubocop/cop/dev_doc/rails/application_model_base.rb
|
|
136
138
|
- lib/rubocop/cop/dev_doc/rails/application_record_transaction.rb
|
|
137
139
|
- lib/rubocop/cop/dev_doc/rails/avoid_bypassing_validation.rb
|
|
138
140
|
- lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb
|