rubocop-bottrall 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0a08ed9eac30511ec7903e3d93b3cab636a0eff14f68382bc25803f6f915949
4
+ data.tar.gz: 947e300fe8c054f27a9ff33cd04a2ee08e5c97440f9a6ad3e877cd7f57d04e3c
5
+ SHA512:
6
+ metadata.gz: c2a290b46ed43cb27c02fe83abf86cd2ccfa53c5d3b1c7074084f6f0acfce31a9857c298633662f0f71ec34fe77bebc47bce860d77287c26ecb05f3cc4c62876
7
+ data.tar.gz: cc0549fd0868c0ba96fe076d95a78a2c82e97b4409b41f780aa6d93d6a259288bfa491b75be309c2ce01c217d691850ca78ab1cefe3a754542acc18945b17b56
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * add rubocop-bottrall with the shared Ruby style and cops ([#1](https://github.com/bottrall/blueprint/issues/1)) ([b693705](https://github.com/bottrall/blueprint/commit/b6937056c768e6d69e55e0ac60ca846f7ce3dcf1))
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jake Bottrall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # rubocop-bottrall
2
+
3
+ The shared Ruby style for bottrall projects, as a [RuboCop](https://rubocop.org) plugin. One gem brings:
4
+
5
+ - `config/base.yml`: the shared style (new cops enabled, one-argument-per-line layout, `Metrics` off except `Metrics/BlockNesting`, compact class nesting, `extend_self` modules, one assertion per Minitest test), with [rubocop-minitest](https://github.com/rubocop/rubocop-minitest), [rubocop-performance](https://github.com/rubocop/rubocop-performance) and [rubocop-rake](https://github.com/rubocop/rubocop-rake) as dependencies and plugins.
6
+ - `config/rbs-inline.yml`: lets `Layout/LeadingCommentSpace` and `Layout/LineLength` accept [rbs-inline](https://github.com/soutaro/rbs-inline) annotations.
7
+ - Three cops in the `Bottrall/` department:
8
+ - `Bottrall/NoInheritance`: compose instead of inheriting. A superclass is allowed only when it's listed in `AllowedParents`, its name ends in `Error`, or it's a core class that is `Exception` or a `StandardError` descendant.
9
+ - `Bottrall/NoMocking`: no `stub`, no `Minitest::Mock`, and (for code under `OwnedNamespaces`) no methods patched in with `define_singleton_method` / `define_method`. Inject a real lightweight fake instead. Only checks test files (`**/test/**/*`, `**/*_test.rb`).
10
+ - `Bottrall/RequireDisableReason`: every `rubocop:disable` / `rubocop:todo` ends with `-- <reason>`.
11
+
12
+ Requires RuboCop 1.72 or later (the plugin system) and Ruby 3.3 or later.
13
+
14
+ ## Usage
15
+
16
+ Add the gem to your `Gemfile`:
17
+
18
+ ```ruby
19
+ gem 'rubocop-bottrall', require: false
20
+ ```
21
+
22
+ Then point `.rubocop.yml` at it:
23
+
24
+ ```yaml
25
+ plugins:
26
+ - rubocop-bottrall
27
+
28
+ inherit_gem:
29
+ rubocop-bottrall:
30
+ - config/base.yml
31
+ - config/rbs-inline.yml # only if the project writes rbs-inline annotations
32
+
33
+ inherit_mode:
34
+ merge:
35
+ - Exclude
36
+ ```
37
+
38
+ `inherit_mode: merge: [Exclude]` adds your own `AllCops: Exclude` entries to the shared ones (`bin/`, `vendor/`, `sig/`, `pkg/`, `tmp/`, `.claude/`, `node_modules/`) instead of replacing them.
39
+
40
+ ### Configuring the cops
41
+
42
+ `Bottrall/NoInheritance` allows no parents beyond errors until you list your framework-mandated ones, by fully qualified constant name:
43
+
44
+ ```yaml
45
+ Bottrall/NoInheritance:
46
+ AllowedParents:
47
+ - ApplicationRecord
48
+ - ApplicationController
49
+ ```
50
+
51
+ For a one-off parent, use a scoped disable with a reason instead: `# rubocop:disable Bottrall/NoInheritance -- <reason>`.
52
+
53
+ `Bottrall/NoMocking` flags `stub` and `Minitest::Mock` everywhere. To also flag methods patched onto your own code (`MyGem::Client.define_singleton_method(...)`, `MyGem::Client.singleton_class.define_method(...)`, or `define_singleton_method` on a local built from `MyGem::...` in the same test), list the namespaces you own:
54
+
55
+ ```yaml
56
+ Bottrall/NoMocking:
57
+ OwnedNamespaces:
58
+ - MyGem
59
+ ```
60
+
61
+ ## Development
62
+
63
+ | Command | What it does |
64
+ |---|---|
65
+ | `bin/setup` | Install dependencies |
66
+ | `bin/test [paths] [minitest flags]` | Run the tests |
67
+ | `bin/lint [--fix] [paths]` | Lint the gem with its own plugin and `config/base.yml` |
68
+ | `bin/build` | Build the gem into `pkg/`, install it into an empty `GEM_HOME`, and check it loads |
69
+ | `bin/check` | `bin/lint --fix`, then `bin/test` |
70
+ | `bin/ci` | Exactly what CI runs: tests, lint, build |
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome at https://github.com/bottrall/blueprint.
75
+
76
+ ## License
77
+
78
+ Released under the [MIT License](LICENSE).
data/config/base.yml ADDED
@@ -0,0 +1,60 @@
1
+ # The shared Ruby style. A project inherits it with
2
+ #
3
+ # plugins:
4
+ # - rubocop-bottrall
5
+ # inherit_gem:
6
+ # rubocop-bottrall:
7
+ # - config/base.yml
8
+ # inherit_mode:
9
+ # merge:
10
+ # - Exclude
11
+
12
+ plugins:
13
+ - rubocop-minitest
14
+ - rubocop-performance
15
+ - rubocop-rake
16
+
17
+ AllCops:
18
+ NewCops: enable
19
+ Exclude:
20
+ - 'bin/**/*'
21
+ - 'vendor/**/*'
22
+ - '**/sig/**/*'
23
+ - 'pkg/**/*'
24
+ - 'tmp/**/*'
25
+ - '.claude/**/*'
26
+ - 'node_modules/**/*'
27
+
28
+ Layout/FirstMethodArgumentLineBreak:
29
+ Enabled: true
30
+
31
+ Layout/FirstMethodParameterLineBreak:
32
+ Enabled: true
33
+
34
+ Layout/MultilineMethodArgumentLineBreaks:
35
+ Enabled: true
36
+
37
+ Layout/MultilineMethodParameterLineBreaks:
38
+ Enabled: true
39
+
40
+ Layout/RedundantLineBreak:
41
+ Enabled: true
42
+ InspectBlocks: false
43
+
44
+ Metrics:
45
+ Enabled: false
46
+
47
+ Metrics/BlockNesting:
48
+ Enabled: true
49
+
50
+ Minitest/MultipleAssertions:
51
+ Max: 1
52
+
53
+ Style/ClassAndModuleChildren:
54
+ EnforcedStyle: compact
55
+
56
+ Style/Documentation:
57
+ Enabled: false
58
+
59
+ Style/ModuleFunction:
60
+ EnforcedStyle: extend_self
@@ -0,0 +1,32 @@
1
+ # Defaults for the Bottrall/ cops, loaded by RuboCop::Bottrall::Plugin.
2
+
3
+ Bottrall/NoInheritance:
4
+ Description: >-
5
+ Compose instead of inheriting: a superclass is allowed only when it's an
6
+ error class or a framework-mandated parent listed in AllowedParents.
7
+ Enabled: true
8
+ VersionAdded: '0.1'
9
+ # Framework-mandated parents, as fully qualified constant names
10
+ # (e.g. `ApplicationRecord`, `RuboCop::Cop::Base`).
11
+ AllowedParents: []
12
+
13
+ Bottrall/NoMocking:
14
+ Description: >-
15
+ Don't stub, use Minitest::Mock, or patch methods onto code you own:
16
+ inject a real lightweight fake instead.
17
+ Enabled: true
18
+ VersionAdded: '0.1'
19
+ Include:
20
+ - '**/test/**/*'
21
+ - '**/*_test.rb'
22
+ # Namespaces whose constants (and locals built from them) may not have
23
+ # methods patched in with define_singleton_method / define_method. With
24
+ # none listed, only `stub` and `Minitest::Mock` are flagged.
25
+ OwnedNamespaces: []
26
+
27
+ Bottrall/RequireDisableReason:
28
+ Description: >-
29
+ Every `rubocop:disable` / `rubocop:todo` directive carries a reason
30
+ after ` -- `.
31
+ Enabled: true
32
+ VersionAdded: '0.1'
@@ -0,0 +1,7 @@
1
+ # For projects that write rbs-inline annotations (`#: Type`, `# @rbs ...`).
2
+
3
+ Layout/LeadingCommentSpace:
4
+ AllowRBSInlineAnnotation: true
5
+
6
+ Layout/LineLength:
7
+ AllowRBSInlineAnnotation: true
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+
5
+ # Registers the cops' defaults (config/default.yml) with RuboCop's plugin
6
+ # system, so `plugins: [rubocop-bottrall]` is all a project needs to load them.
7
+ class RuboCop::Bottrall::Plugin < LintRoller::Plugin
8
+ def about
9
+ LintRoller::About.new(
10
+ name: 'rubocop-bottrall',
11
+ version: RuboCop::Bottrall::VERSION,
12
+ homepage: 'https://github.com/bottrall/blueprint/tree/main/ruby',
13
+ description: 'The shared Ruby style and custom cops for bottrall projects.'
14
+ )
15
+ end
16
+
17
+ def supported?(context)
18
+ context.engine == :rubocop
19
+ end
20
+
21
+ def rules(_context)
22
+ LintRoller::Rules.new(
23
+ type: :path,
24
+ config_format: :rubocop,
25
+ value: Pathname.new(__dir__).join('../../../config/default.yml')
26
+ )
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Bottrall
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The plugin's namespace; its cops live under RuboCop::Cop::Bottrall.
4
+ module RuboCop::Bottrall
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Flags a class with a superclass, unless the superclass is
4
+ #
5
+ # - listed in `AllowedParents` (framework-mandated parents),
6
+ # - named `...Error`, or
7
+ # - a core class that is `Exception` or descends from `StandardError`.
8
+ class RuboCop::Cop::Bottrall::NoInheritance < RuboCop::Cop::Base
9
+ MSG = "Compose, don't inherit: pass collaborators in, share code through a mixin, and describe " \
10
+ 'contracts as RBS interfaces. Only errors and the framework parents in AllowedParents may be ' \
11
+ 'superclasses; for a one-off framework parent, add a scoped disable with a reason.'
12
+
13
+ def on_class(node)
14
+ parent = node.parent_class
15
+ return unless parent
16
+ return if parent.const_type? && allowed?(parent.const_name.delete_prefix('::'))
17
+
18
+ add_offense(parent)
19
+ end
20
+
21
+ private
22
+
23
+ def allowed?(name)
24
+ allowed_parents.include?(name) || name.end_with?('Error') || core_error?(name)
25
+ end
26
+
27
+ def allowed_parents
28
+ Array(cop_config['AllowedParents']).map { |parent| parent.delete_prefix('::') }
29
+ end
30
+
31
+ # Only core classes, defined in C, resolve the same in every process: a
32
+ # gem's or the project's own error classes aren't loaded when the cop runs.
33
+ def core_error?(name)
34
+ return false unless Object.const_defined?(name) && Object.const_source_location(name) == []
35
+
36
+ parent = Object.const_get(name)
37
+ parent.is_a?(Class) && (parent.equal?(Exception) || parent <= StandardError)
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Flags `stub`, `Minitest::Mock`, and, for code under one of the
4
+ # `OwnedNamespaces`, methods patched in with `define_singleton_method` or
5
+ # `define_method` (directly, through `singleton_class`, or via `send`).
6
+ class RuboCop::Cop::Bottrall::NoMocking < RuboCop::Cop::Base
7
+ MSG = "Don't mock code you own: inject a real lightweight fake instead. " \
8
+ 'Mock a genuine third party only at its API boundary.'
9
+
10
+ def_node_matcher :singleton_patch, <<~PATTERN
11
+ {
12
+ (send $_ :define_singleton_method ...)
13
+ (send (send $_ :singleton_class) :define_method ...)
14
+ (send (send $_ :singleton_class) {:send :__send__} (sym :define_method) ...)
15
+ }
16
+ PATTERN
17
+
18
+ def_node_matcher :class_patch, <<~PATTERN
19
+ {
20
+ (send $const :define_method ...)
21
+ (send $const {:send :__send__} (sym :define_method) ...)
22
+ }
23
+ PATTERN
24
+
25
+ def on_const(node)
26
+ add_offense(node) if node.const_name.delete_prefix('::') == 'Minitest::Mock'
27
+ end
28
+
29
+ def on_send(node)
30
+ add_offense(node) if node.method?(:stub) || patches_owned_code?(node)
31
+ end
32
+
33
+ private
34
+
35
+ def owned_namespaces
36
+ @owned_namespaces ||= Array(cop_config['OwnedNamespaces']).map { |namespace| namespace.delete_prefix('::') }
37
+ end
38
+
39
+ def patches_owned_code?(node)
40
+ return false if owned_namespaces.empty?
41
+
42
+ receiver = singleton_patch(node) || class_patch(node)
43
+ receiver ? owned?(receiver, node) : false
44
+ end
45
+
46
+ def owned?(receiver, node)
47
+ case receiver.type
48
+ when :const then owned_constant?(receiver)
49
+ when :lvar then built_from_owned_constant?(receiver, node)
50
+ else false
51
+ end
52
+ end
53
+
54
+ def owned_constant?(node)
55
+ name = node.const_name.delete_prefix('::')
56
+ owned_namespaces.any? { |namespace| name == namespace || name.start_with?("#{namespace}::") }
57
+ end
58
+
59
+ # Only a local built in the patch's own scope has a type the cop can see:
60
+ # a block parameter or a helper's return value could be anything.
61
+ def built_from_owned_constant?(receiver, node)
62
+ patch = node.parent&.any_block_type? && node.parent.send_node.equal?(node) ? node.parent : node
63
+ scope = patch.each_ancestor(:any_block, :any_def).first || processed_source.ast
64
+ scope.each_descendant(:lvasgn).any? do |assignment|
65
+ value = assignment.expression
66
+ assignment.name == receiver.children.first &&
67
+ value&.send_type? && value.receiver&.const_type? && owned_constant?(value.receiver)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Every `rubocop:disable` / `rubocop:todo` has to say why: end it with `-- <reason>`.
4
+ class RuboCop::Cop::Bottrall::RequireDisableReason < RuboCop::Cop::Base
5
+ MSG = 'Add a reason to the disable directive: end it with `-- <reason>`.'
6
+
7
+ DISABLE_DIRECTIVE = /\A#\s*rubocop\s*:\s*(?:disable|todo)\b/
8
+ REASON = /\s--\s*\S/
9
+
10
+ def on_new_investigation
11
+ processed_source.comments
12
+ .select { |comment| DISABLE_DIRECTIVE.match?(comment.text) && !REASON.match?(comment.text) }
13
+ .each { |comment| add_offense(comment) }
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Bottrall/` cop department.
4
+ module RuboCop::Cop::Bottrall
5
+ end
6
+
7
+ require_relative 'bottrall/no_inheritance'
8
+ require_relative 'bottrall/no_mocking'
9
+ require_relative 'bottrall/require_disable_reason'
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ require_relative 'rubocop/bottrall'
6
+ require_relative 'rubocop/bottrall/version'
7
+ require_relative 'rubocop/bottrall/plugin'
8
+ require_relative 'rubocop/cop/bottrall'
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-bottrall
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Bottrall
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lint_roller
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.72'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.72'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rubocop-minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.37'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.37'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rubocop-performance
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.24'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.24'
74
+ - !ruby/object:Gem::Dependency
75
+ name: rubocop-rake
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '0.7'
81
+ type: :runtime
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.7'
88
+ description: A RuboCop plugin bundling a shared style (config/base.yml), an rbs-inline
89
+ layer (config/rbs-inline.yml), and the Bottrall/NoInheritance, Bottrall/NoMocking
90
+ and Bottrall/RequireDisableReason cops.
91
+ email:
92
+ - jakebottrall@gmail.com
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - CHANGELOG.md
98
+ - LICENSE
99
+ - README.md
100
+ - config/base.yml
101
+ - config/default.yml
102
+ - config/rbs-inline.yml
103
+ - lib/rubocop-bottrall.rb
104
+ - lib/rubocop/bottrall.rb
105
+ - lib/rubocop/bottrall/plugin.rb
106
+ - lib/rubocop/bottrall/version.rb
107
+ - lib/rubocop/cop/bottrall.rb
108
+ - lib/rubocop/cop/bottrall/no_inheritance.rb
109
+ - lib/rubocop/cop/bottrall/no_mocking.rb
110
+ - lib/rubocop/cop/bottrall/require_disable_reason.rb
111
+ homepage: https://github.com/bottrall/blueprint/tree/main/ruby#readme
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ homepage_uri: https://github.com/bottrall/blueprint/tree/main/ruby#readme
116
+ source_code_uri: https://github.com/bottrall/blueprint/tree/main/ruby
117
+ changelog_uri: https://github.com/bottrall/blueprint/blob/main/ruby/CHANGELOG.md
118
+ bug_tracker_uri: https://github.com/bottrall/blueprint/issues
119
+ rubygems_mfa_required: 'true'
120
+ default_lint_roller_plugin: RuboCop::Bottrall::Plugin
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '3.3'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 4.0.20
136
+ specification_version: 4
137
+ summary: The shared RuboCop style and custom cops for bottrall projects.
138
+ test_files: []