rubocop-sane 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +110 -0
- data/.vscode/cspell.json +28 -0
- data/.vscode/project-words.txt +6 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +77 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/config/default.yml +33 -0
- data/lib/rubocop/cop/sane/conditional_assignment_allow_ternary.rb +105 -0
- data/lib/rubocop/cop/sane/disallow_methods.rb +98 -0
- data/lib/rubocop/cop/sane/empty_line_before_comment.rb +133 -0
- data/lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb +357 -0
- data/lib/rubocop/cop/sane/no_method_call_after_end.rb +78 -0
- data/lib/rubocop/cop/sane_cops.rb +7 -0
- data/lib/rubocop/sane/plugin.rb +31 -0
- data/lib/rubocop/sane/version.rb +7 -0
- data/lib/rubocop/sane.rb +11 -0
- data/lib/rubocop-sane.rb +8 -0
- data/sig/rubocop/sane.rbs +6 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 46577dac32dde6106d874ce5fa85e7a54a94d589e075b54d281d7b68d9649e19
|
|
4
|
+
data.tar.gz: 8e1280ecc3bee57ed3cb65924ee86ce9b02c6032e74ee58864989fdf9b4cdac9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 70e429be341dc7b52167483547162dcf4f52a99f278c98782eec989d0a8a061b72f40cd8dfd5d8b9d56cdfd79b9eb3c1934c353e1de29b435f46790b9c1586eb
|
|
7
|
+
data.tar.gz: 58dc631f79eb2cb9a022359fe0ba690ae55607b927b840113f469f648909f205619d5ef15fd427d88a24ed1ddac43fd7156b0aa7c1576385585951b5228706ea
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rake
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
- rubocop-performance
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
DisplayCopNames: true
|
|
8
|
+
DisplayStyleGuide: true
|
|
9
|
+
NewCops: enable
|
|
10
|
+
TargetRubyVersion: 3.2
|
|
11
|
+
Exclude:
|
|
12
|
+
- sorbet/**/*.rbi
|
|
13
|
+
|
|
14
|
+
# Standard naming for RuboCop extension gems
|
|
15
|
+
Naming/FileName:
|
|
16
|
+
Exclude:
|
|
17
|
+
- lib/rubocop-sane.rb
|
|
18
|
+
|
|
19
|
+
Layout/FirstArrayElementIndentation:
|
|
20
|
+
EnforcedStyle: consistent
|
|
21
|
+
|
|
22
|
+
Layout/FirstHashElementIndentation:
|
|
23
|
+
EnforcedStyle: consistent
|
|
24
|
+
|
|
25
|
+
Layout/MultilineMethodCallIndentation:
|
|
26
|
+
EnforcedStyle: indented
|
|
27
|
+
|
|
28
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
|
29
|
+
EnforcedStyle: no_space
|
|
30
|
+
|
|
31
|
+
Lint/EmptyFile:
|
|
32
|
+
Enabled: true
|
|
33
|
+
|
|
34
|
+
Metrics/AbcSize:
|
|
35
|
+
Max: 20
|
|
36
|
+
Exclude:
|
|
37
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
38
|
+
|
|
39
|
+
Metrics/BlockLength:
|
|
40
|
+
Exclude:
|
|
41
|
+
- spec/**/*_spec.rb
|
|
42
|
+
|
|
43
|
+
Metrics/ClassLength:
|
|
44
|
+
Max: 150
|
|
45
|
+
Exclude:
|
|
46
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
47
|
+
|
|
48
|
+
Metrics/CyclomaticComplexity:
|
|
49
|
+
Max: 10
|
|
50
|
+
Exclude:
|
|
51
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
52
|
+
|
|
53
|
+
Metrics/MethodLength:
|
|
54
|
+
Max: 20
|
|
55
|
+
|
|
56
|
+
Metrics/PerceivedComplexity:
|
|
57
|
+
Max: 10
|
|
58
|
+
Exclude:
|
|
59
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
60
|
+
|
|
61
|
+
Naming/PredicatePrefix:
|
|
62
|
+
Enabled: false
|
|
63
|
+
|
|
64
|
+
RSpec/ExampleLength:
|
|
65
|
+
Max: 20
|
|
66
|
+
|
|
67
|
+
RSpec/MultipleDescribes:
|
|
68
|
+
Enabled: false
|
|
69
|
+
|
|
70
|
+
RSpec/MultipleExpectations:
|
|
71
|
+
Max: 15
|
|
72
|
+
|
|
73
|
+
RSpec/MultipleMemoizedHelpers:
|
|
74
|
+
Max: 10
|
|
75
|
+
|
|
76
|
+
RSpec/NestedGroups:
|
|
77
|
+
Max: 5
|
|
78
|
+
|
|
79
|
+
Style/ClassVars:
|
|
80
|
+
Enabled: false
|
|
81
|
+
|
|
82
|
+
Style/ConditionalAssignment:
|
|
83
|
+
Enabled: false
|
|
84
|
+
|
|
85
|
+
Style/Documentation:
|
|
86
|
+
Enabled: false
|
|
87
|
+
|
|
88
|
+
Style/GuardClause:
|
|
89
|
+
Enabled: false
|
|
90
|
+
|
|
91
|
+
Style/StringLiterals:
|
|
92
|
+
EnforcedStyle: double_quotes
|
|
93
|
+
|
|
94
|
+
Style/SymbolArray:
|
|
95
|
+
EnforcedStyle: brackets
|
|
96
|
+
|
|
97
|
+
Style/TrailingCommaInArguments:
|
|
98
|
+
EnforcedStyleForMultiline: diff_comma
|
|
99
|
+
|
|
100
|
+
Style/TrailingCommaInArrayLiteral:
|
|
101
|
+
EnforcedStyleForMultiline: consistent_comma
|
|
102
|
+
|
|
103
|
+
Style/TrailingCommaInHashLiteral:
|
|
104
|
+
EnforcedStyleForMultiline: consistent_comma
|
|
105
|
+
|
|
106
|
+
Style/WordArray:
|
|
107
|
+
EnforcedStyle: brackets
|
|
108
|
+
|
|
109
|
+
Style/NumericPredicate:
|
|
110
|
+
Enabled: false
|
data/.vscode/cspell.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
|
3
|
+
"version": "0.2",
|
|
4
|
+
"dictionaryDefinitions": [
|
|
5
|
+
{
|
|
6
|
+
"name": "project-words",
|
|
7
|
+
"path": "./project-words.txt",
|
|
8
|
+
"addWords": true
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"dictionaries": [
|
|
12
|
+
"project-words"
|
|
13
|
+
],
|
|
14
|
+
"ignorePaths": [
|
|
15
|
+
".idea",
|
|
16
|
+
".ruby-lsp",
|
|
17
|
+
"log",
|
|
18
|
+
"storage",
|
|
19
|
+
"tmp",
|
|
20
|
+
"vendor",
|
|
21
|
+
"*.svg",
|
|
22
|
+
"*.dump",
|
|
23
|
+
"*.enc",
|
|
24
|
+
"Gemfile.lock",
|
|
25
|
+
"/.vscode/project-words.txt",
|
|
26
|
+
"spec/fixtures/**/*"
|
|
27
|
+
]
|
|
28
|
+
}
|
data/CHANGELOG.md
ADDED
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
rubocop-sane is a RuboCop extension gem that provides custom cops for enforcing sensible Ruby coding conventions. It uses the LintRoller plugin system for integration with RuboCop.
|
|
8
|
+
|
|
9
|
+
## Common Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install dependencies
|
|
13
|
+
bin/setup
|
|
14
|
+
|
|
15
|
+
# Run all tests
|
|
16
|
+
bundle exec rake spec
|
|
17
|
+
|
|
18
|
+
# Run a single test file
|
|
19
|
+
bundle exec rspec spec/rubocop/cop/sane/disallow_methods_spec.rb
|
|
20
|
+
|
|
21
|
+
# Run a single test by line number
|
|
22
|
+
bundle exec rspec spec/rubocop/cop/sane/disallow_methods_spec.rb:42
|
|
23
|
+
|
|
24
|
+
# Run RuboCop linting
|
|
25
|
+
bundle exec rubocop
|
|
26
|
+
|
|
27
|
+
# Run all checks (tests + rubocop)
|
|
28
|
+
bundle exec rake
|
|
29
|
+
|
|
30
|
+
# Interactive console
|
|
31
|
+
bin/console
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
### Entry Point & Plugin System
|
|
37
|
+
|
|
38
|
+
- `lib/rubocop-sane.rb` - Main entry point that loads all components
|
|
39
|
+
- `lib/rubocop/sane/plugin.rb` - LintRoller plugin that integrates with RuboCop's plugin system
|
|
40
|
+
- `config/default.yml` - Default cop configurations
|
|
41
|
+
|
|
42
|
+
### Cop Structure
|
|
43
|
+
|
|
44
|
+
All cops live under `lib/rubocop/cop/sane/` and follow RuboCop's cop pattern:
|
|
45
|
+
- Inherit from `RuboCop::Cop::Base`
|
|
46
|
+
- Use `extend AutoCorrector` for auto-fix support
|
|
47
|
+
- Implement `on_*` methods (e.g., `on_send`, `on_if`) to visit AST nodes
|
|
48
|
+
|
|
49
|
+
### Current Cops
|
|
50
|
+
|
|
51
|
+
1. **DisallowMethods** - Configurable cop for method replacements and prohibitions with auto-correct
|
|
52
|
+
2. **ConditionalAssignmentAllowTernary** - Enforces assignment inside conditions but allows ternary operators
|
|
53
|
+
3. **EmptyLineBeforeComment** - Requires blank line before comments (except after block starts, class/method definitions, etc.)
|
|
54
|
+
4. **EmptyLinesAroundMultilineBlock** - Enforces empty lines around multiline if/case/block statements
|
|
55
|
+
|
|
56
|
+
### Testing
|
|
57
|
+
|
|
58
|
+
Tests use RuboCop's `ExpectOffense` helper which provides a DSL for testing cop behavior:
|
|
59
|
+
```ruby
|
|
60
|
+
expect_offense(<<~RUBY)
|
|
61
|
+
foo = if bar
|
|
62
|
+
^^^^^^^^^^^^ Move the assignment inside the `if` branch.
|
|
63
|
+
1
|
|
64
|
+
else
|
|
65
|
+
2
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `^` characters mark where the offense should be detected.
|
|
71
|
+
|
|
72
|
+
## Adding a New Cop
|
|
73
|
+
|
|
74
|
+
1. Create cop file in `lib/rubocop/cop/sane/your_cop.rb`
|
|
75
|
+
2. Add require in `lib/rubocop/cop/sane_cops.rb`
|
|
76
|
+
3. Add configuration in `config/default.yml`
|
|
77
|
+
4. Create spec in `spec/rubocop/cop/sane/your_cop_spec.rb`
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"rubocop-sane" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["678665+akodkod@users.noreply.github.com"](mailto:"678665+akodkod@users.noreply.github.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andrew Kodkod
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Rubocop::Sane
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rubocop/sane`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rubocop-sane. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/rubocop-sane/blob/main/CODE_OF_CONDUCT.md).
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
40
|
+
|
|
41
|
+
## Code of Conduct
|
|
42
|
+
|
|
43
|
+
Everyone interacting in the Rubocop::Sane project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rubocop-sane/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/config/default.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Sane:
|
|
2
|
+
Enabled: true
|
|
3
|
+
DocumentationBaseURL: https://github.com/akodkod/rubocop-sane
|
|
4
|
+
|
|
5
|
+
Sane/DisallowMethods:
|
|
6
|
+
Description: "Enforces method replacements and prohibitions."
|
|
7
|
+
Enabled: true
|
|
8
|
+
VersionAdded: "0.1.0"
|
|
9
|
+
SafeAutoCorrect: false
|
|
10
|
+
ReplaceMethods: {}
|
|
11
|
+
ProhibitedMethods: {}
|
|
12
|
+
|
|
13
|
+
Sane/ConditionalAssignmentAllowTernary:
|
|
14
|
+
Description: "Enforces assignment inside conditions, but allows ternary operators."
|
|
15
|
+
Enabled: true
|
|
16
|
+
VersionAdded: "0.1.0"
|
|
17
|
+
|
|
18
|
+
Sane/EmptyLineBeforeComment:
|
|
19
|
+
Description: "Enforces empty line before comments, except after block starts."
|
|
20
|
+
Enabled: true
|
|
21
|
+
VersionAdded: "0.1.0"
|
|
22
|
+
SafeAutoCorrect: true
|
|
23
|
+
|
|
24
|
+
Sane/EmptyLinesAroundMultilineBlock:
|
|
25
|
+
Description: "Enforces empty lines before and after multiline blocks."
|
|
26
|
+
Enabled: true
|
|
27
|
+
VersionAdded: "0.1.0"
|
|
28
|
+
SafeAutoCorrect: true
|
|
29
|
+
|
|
30
|
+
Sane/NoMethodCallAfterEnd:
|
|
31
|
+
Description: "Prohibits calling methods directly after `end`."
|
|
32
|
+
Enabled: true
|
|
33
|
+
VersionAdded: "0.1.0"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# This cop enforces `assign_inside_condition` style like
|
|
7
|
+
# `Style/ConditionalAssignment`, but allows ternary operators.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# foo = if condition
|
|
12
|
+
# 1
|
|
13
|
+
# else
|
|
14
|
+
# 2
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # bad
|
|
18
|
+
# foo = case bar
|
|
19
|
+
# when :a then 1
|
|
20
|
+
# when :b then 2
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # good - assignment inside condition
|
|
24
|
+
# if condition
|
|
25
|
+
# foo = 1
|
|
26
|
+
# else
|
|
27
|
+
# foo = 2
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # good - ternary operators are allowed
|
|
31
|
+
# foo = condition ? 1 : 2
|
|
32
|
+
#
|
|
33
|
+
# # good - multiline ternary operators are allowed
|
|
34
|
+
# foo = condition
|
|
35
|
+
# ? 1
|
|
36
|
+
# : 2
|
|
37
|
+
#
|
|
38
|
+
class ConditionalAssignmentAllowTernary < Base
|
|
39
|
+
MSG = "Move the assignment inside the `%<keyword>s` branch."
|
|
40
|
+
|
|
41
|
+
ASSIGNMENT_TYPES = [:lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn].freeze
|
|
42
|
+
|
|
43
|
+
def on_lvasgn(node)
|
|
44
|
+
check_assignment(node)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
alias on_ivasgn on_lvasgn
|
|
48
|
+
alias on_cvasgn on_lvasgn
|
|
49
|
+
alias on_gvasgn on_lvasgn
|
|
50
|
+
alias on_casgn on_lvasgn
|
|
51
|
+
|
|
52
|
+
def on_masgn(node)
|
|
53
|
+
check_assignment(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def on_op_asgn(node)
|
|
57
|
+
check_assignment(node)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_or_asgn(node)
|
|
61
|
+
check_assignment(node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_and_asgn(node)
|
|
65
|
+
check_assignment(node)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def check_assignment(node)
|
|
71
|
+
return unless assignment_to_conditional?(node)
|
|
72
|
+
|
|
73
|
+
rhs = extract_rhs(node)
|
|
74
|
+
return unless rhs
|
|
75
|
+
|
|
76
|
+
keyword = rhs.if_type? ? rhs.keyword : "case"
|
|
77
|
+
add_offense(node, message: format(MSG, keyword: keyword))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def assignment_to_conditional?(node)
|
|
81
|
+
rhs = extract_rhs(node)
|
|
82
|
+
return false unless rhs
|
|
83
|
+
|
|
84
|
+
if rhs.if_type?
|
|
85
|
+
# Allow ternary operators, only flag if/else blocks
|
|
86
|
+
return false if rhs.ternary?
|
|
87
|
+
|
|
88
|
+
rhs.else?
|
|
89
|
+
else
|
|
90
|
+
rhs.case_type?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def extract_rhs(node)
|
|
95
|
+
case node.type
|
|
96
|
+
when :lvasgn, :ivasgn, :cvasgn, :gvasgn, :masgn, :or_asgn, :and_asgn
|
|
97
|
+
node.children[1]
|
|
98
|
+
when :casgn, :op_asgn
|
|
99
|
+
node.children[2]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces method replacements and prohibitions.
|
|
7
|
+
#
|
|
8
|
+
# This cop checks for usage of specified methods and either suggests
|
|
9
|
+
# replacements (with auto-correction) or prohibits usage entirely.
|
|
10
|
+
#
|
|
11
|
+
# @example ReplaceMethods configuration
|
|
12
|
+
# # .rubocop.yml
|
|
13
|
+
# # Sane/DisallowMethods:
|
|
14
|
+
# # ReplaceMethods:
|
|
15
|
+
# # deliver_now:
|
|
16
|
+
# # with: deliver_later
|
|
17
|
+
# # reason: "`deliver_later` sends the email via background job"
|
|
18
|
+
#
|
|
19
|
+
# # bad
|
|
20
|
+
# UserMailer.welcome(user).deliver_now
|
|
21
|
+
#
|
|
22
|
+
# # good
|
|
23
|
+
# UserMailer.welcome(user).deliver_later
|
|
24
|
+
#
|
|
25
|
+
# @example ProhibitedMethods configuration
|
|
26
|
+
# # .rubocop.yml
|
|
27
|
+
# # Sane/DisallowMethods:
|
|
28
|
+
# # ProhibitedMethods:
|
|
29
|
+
# # dangerous_method:
|
|
30
|
+
# # reason: "This method is deprecated and unsafe"
|
|
31
|
+
#
|
|
32
|
+
# # bad
|
|
33
|
+
# obj.dangerous_method
|
|
34
|
+
#
|
|
35
|
+
class DisallowMethods < Base
|
|
36
|
+
extend AutoCorrector
|
|
37
|
+
|
|
38
|
+
REPLACEABLE_METHOD_MSG = "You should use `%<with>s` instead of `%<method_name>s` because %<reason>s"
|
|
39
|
+
PROHIBITED_METHOD_MSG = "You should not use `%<method_name>s` because %<reason>s"
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
method_name = node.method_name
|
|
43
|
+
replaceable = replace_methods[method_name]
|
|
44
|
+
prohibited = prohibited_methods[method_name]
|
|
45
|
+
|
|
46
|
+
if replaceable
|
|
47
|
+
handle_replaceable_method(node, method_name, replaceable)
|
|
48
|
+
elsif prohibited
|
|
49
|
+
handle_prohibited_method(node, method_name, prohibited)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def handle_replaceable_method(node, method_name, config)
|
|
56
|
+
message = format(
|
|
57
|
+
REPLACEABLE_METHOD_MSG,
|
|
58
|
+
method_name: method_name,
|
|
59
|
+
with: config["with"],
|
|
60
|
+
reason: config["reason"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
add_offense(node, severity: :error, message: message) do |corrector|
|
|
64
|
+
corrector.replace(node.loc.selector, config["with"])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def handle_prohibited_method(node, method_name, config)
|
|
69
|
+
message = format(
|
|
70
|
+
PROHIBITED_METHOD_MSG,
|
|
71
|
+
method_name: method_name,
|
|
72
|
+
reason: config["reason"],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
add_offense(node, severity: :error, message: message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def replace_methods
|
|
79
|
+
@replace_methods ||= build_method_hash(cop_config.fetch("ReplaceMethods", {}))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def prohibited_methods
|
|
83
|
+
@prohibited_methods ||= build_method_hash(cop_config.fetch("ProhibitedMethods", {}))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_method_hash(config)
|
|
87
|
+
return {} unless config.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
config.transform_keys(&:to_sym)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def safe_autocorrect?
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Enforces an empty line before comments, except when the previous line
|
|
7
|
+
# is the start of a block, class, method, or another comment.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# foo = 1
|
|
12
|
+
# # This is a comment
|
|
13
|
+
# bar = 2
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# foo = 1
|
|
17
|
+
#
|
|
18
|
+
# # This is a comment
|
|
19
|
+
# bar = 2
|
|
20
|
+
#
|
|
21
|
+
# # good - after block/class/method start
|
|
22
|
+
# def foo
|
|
23
|
+
# # This comment doesn't need a blank line
|
|
24
|
+
# bar
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # good - consecutive comments
|
|
28
|
+
# # First comment
|
|
29
|
+
# # Second comment
|
|
30
|
+
#
|
|
31
|
+
# # good - after control structure start
|
|
32
|
+
# if condition
|
|
33
|
+
# # Comment inside if
|
|
34
|
+
# do_something
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
class EmptyLineBeforeComment < Base
|
|
38
|
+
extend AutoCorrector
|
|
39
|
+
|
|
40
|
+
MSG = "Add empty line before comment."
|
|
41
|
+
|
|
42
|
+
def on_new_investigation
|
|
43
|
+
processed_source.comments.each do |comment|
|
|
44
|
+
check_comment(comment)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def check_comment(comment)
|
|
51
|
+
return if inline_comment?(comment)
|
|
52
|
+
return if first_line?(comment)
|
|
53
|
+
return if preceded_by_empty_line?(comment)
|
|
54
|
+
return if preceded_by_comment?(comment)
|
|
55
|
+
return if preceded_by_block_start?(comment)
|
|
56
|
+
|
|
57
|
+
add_offense(comment) do |corrector|
|
|
58
|
+
corrector.insert_before(
|
|
59
|
+
comment.source_range.with(
|
|
60
|
+
begin_pos: comment.source_range.begin_pos - comment.source_range.column,
|
|
61
|
+
),
|
|
62
|
+
"\n",
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inline_comment?(comment)
|
|
68
|
+
# Check if there's code before the comment on the same line
|
|
69
|
+
comment_line = processed_source.lines[comment.loc.line - 1]
|
|
70
|
+
return false unless comment_line
|
|
71
|
+
|
|
72
|
+
# Get the portion of the line before the comment
|
|
73
|
+
before_comment = comment_line[0...comment.loc.column]
|
|
74
|
+
before_comment && !before_comment.strip.empty?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def first_line?(comment)
|
|
78
|
+
comment.loc.line == 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def preceded_by_empty_line?(comment)
|
|
82
|
+
prev_line_number = comment.loc.line - 1
|
|
83
|
+
return true if prev_line_number < 1
|
|
84
|
+
|
|
85
|
+
prev_line = processed_source.lines[prev_line_number - 1]
|
|
86
|
+
prev_line.nil? || prev_line.strip.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def preceded_by_comment?(comment)
|
|
90
|
+
prev_line_number = comment.loc.line - 1
|
|
91
|
+
return false if prev_line_number < 1
|
|
92
|
+
|
|
93
|
+
processed_source.comments.any? { |c| c.loc.line == prev_line_number }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def preceded_by_block_start?(comment)
|
|
97
|
+
prev_line_number = comment.loc.line - 1
|
|
98
|
+
return false if prev_line_number < 1
|
|
99
|
+
|
|
100
|
+
prev_line = processed_source.lines[prev_line_number - 1]
|
|
101
|
+
return false unless prev_line
|
|
102
|
+
|
|
103
|
+
block_start_pattern?(prev_line)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def block_start_pattern?(line)
|
|
107
|
+
stripped = line.strip
|
|
108
|
+
|
|
109
|
+
# Class, module, method definitions
|
|
110
|
+
return true if stripped.match?(/\A(class|module|def)\s/)
|
|
111
|
+
|
|
112
|
+
# Control structures
|
|
113
|
+
return true if stripped.match?(/\A(if|unless|case|while|until|for|begin)\s/)
|
|
114
|
+
return true if stripped == "begin"
|
|
115
|
+
|
|
116
|
+
# Block openers (do or {)
|
|
117
|
+
return true if stripped.end_with?(" do", " do |", "\tdo")
|
|
118
|
+
return true if stripped.match?(/do\s*\|[^|]*\|\s*\z/)
|
|
119
|
+
return true if stripped.end_with?("{")
|
|
120
|
+
return true if stripped.match?(/\{\s*\|[^|]*\|\s*\z/)
|
|
121
|
+
|
|
122
|
+
# Rescue/ensure/else/elsif/when inside blocks
|
|
123
|
+
return true if stripped.match?(/\A(rescue|ensure|else|elsif|when)\b/)
|
|
124
|
+
|
|
125
|
+
# Private/protected/public access modifiers
|
|
126
|
+
return true if stripped.match?(/\A(private|protected|public)\s*\z/)
|
|
127
|
+
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# This cop enforces empty lines before and after multiline blocks
|
|
7
|
+
# such as `if/else` and `case/when`.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# work_for = data.work_done_for
|
|
12
|
+
# if data.present?
|
|
13
|
+
# creation_date = date1
|
|
14
|
+
# else
|
|
15
|
+
# creation_date = date2
|
|
16
|
+
# end
|
|
17
|
+
# legal_start_date = date3
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# work_for = data.work_done_for
|
|
21
|
+
#
|
|
22
|
+
# if data.present?
|
|
23
|
+
# creation_date = date1
|
|
24
|
+
# else
|
|
25
|
+
# creation_date = date2
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# legal_start_date = date3
|
|
29
|
+
#
|
|
30
|
+
# # good - no blank line needed at beginning of method
|
|
31
|
+
# def foo
|
|
32
|
+
# if condition
|
|
33
|
+
# bar
|
|
34
|
+
# else
|
|
35
|
+
# baz
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# qux
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # good - no blank line needed at end of method
|
|
42
|
+
# def foo
|
|
43
|
+
# bar
|
|
44
|
+
#
|
|
45
|
+
# if condition
|
|
46
|
+
# baz
|
|
47
|
+
# else
|
|
48
|
+
# qux
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
class EmptyLinesAroundMultilineBlock < Base
|
|
53
|
+
extend AutoCorrector
|
|
54
|
+
|
|
55
|
+
MSG_BEFORE = "Add empty line before multiline `%<keyword>s` block."
|
|
56
|
+
MSG_AFTER = "Add empty line after multiline `%<keyword>s` block."
|
|
57
|
+
|
|
58
|
+
def on_if(node)
|
|
59
|
+
return if node.ternary?
|
|
60
|
+
return if node.modifier_form?
|
|
61
|
+
return if node.elsif? # elsif is part of parent if, not a separate block
|
|
62
|
+
return unless multiline?(node)
|
|
63
|
+
return if part_of_expression?(node)
|
|
64
|
+
|
|
65
|
+
check_empty_line_before(node)
|
|
66
|
+
check_empty_line_after(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def part_of_expression?(node)
|
|
70
|
+
parent = node.parent
|
|
71
|
+
return false unless parent
|
|
72
|
+
|
|
73
|
+
# Part of assignment: foo = if ... end
|
|
74
|
+
return true if assignment_node?(parent)
|
|
75
|
+
|
|
76
|
+
# Part of setter call: obj.foo = if ... end
|
|
77
|
+
return true if parent.send_type? && parent.method_name.to_s.end_with?("=")
|
|
78
|
+
|
|
79
|
+
# Part of method arguments: foo(if ... end)
|
|
80
|
+
return true if parent.send_type? && parent.arguments.include?(node)
|
|
81
|
+
|
|
82
|
+
# Part of array: [if ... end]
|
|
83
|
+
return true if parent.array_type?
|
|
84
|
+
|
|
85
|
+
# Part of hash value: { key: if ... end }
|
|
86
|
+
return true if parent.pair_type?
|
|
87
|
+
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def on_case(node)
|
|
92
|
+
return unless multiline?(node)
|
|
93
|
+
|
|
94
|
+
check_empty_line_before(node)
|
|
95
|
+
check_empty_line_after(node)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias on_case_match on_case
|
|
99
|
+
|
|
100
|
+
def on_block(node)
|
|
101
|
+
return unless multiline?(node)
|
|
102
|
+
return if chained_block?(node) # e.g., expect { ... }.to raise_error
|
|
103
|
+
return if lambda_block?(node) # e.g., -> { ... } or -> do ... end
|
|
104
|
+
|
|
105
|
+
check_empty_line_before(node)
|
|
106
|
+
check_empty_line_after(node)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
alias on_numblock on_block
|
|
110
|
+
|
|
111
|
+
def lambda_block?(node)
|
|
112
|
+
node.send_node&.lambda_literal?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def chained_block?(node)
|
|
116
|
+
# Skip blocks that are part of a method chain
|
|
117
|
+
# e.g., expect { ... }.to raise_error
|
|
118
|
+
# e.g., foo.map { ... }&.join (csend is safe navigation &.)
|
|
119
|
+
parent = node.parent
|
|
120
|
+
return true if (parent&.send_type? || parent&.csend_type?) && parent.receiver == node
|
|
121
|
+
|
|
122
|
+
# Skip blocks that are part of an assignment expression
|
|
123
|
+
# e.g., self.foo = items.map do ... end
|
|
124
|
+
return true if part_of_assignment?(node)
|
|
125
|
+
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def part_of_assignment?(node)
|
|
130
|
+
parent = node.parent
|
|
131
|
+
return false unless parent
|
|
132
|
+
|
|
133
|
+
# Direct assignment: foo = bar.map do...end
|
|
134
|
+
return true if assignment_node?(parent)
|
|
135
|
+
|
|
136
|
+
# Assignment via setter: self.foo = bar.map do...end
|
|
137
|
+
return true if parent.send_type? && parent.method_name.to_s.end_with?("=")
|
|
138
|
+
|
|
139
|
+
# Hash value: { key: items.map do...end }
|
|
140
|
+
return true if parent.pair_type?
|
|
141
|
+
|
|
142
|
+
# Array element: [items.map do...end]
|
|
143
|
+
return true if parent.array_type?
|
|
144
|
+
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def assignment_node?(node)
|
|
149
|
+
[:lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn, :masgn, :op_asgn, :or_asgn, :and_asgn].include?(node.type)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def preceded_by_paired_method?(node, prev_sibling)
|
|
153
|
+
# Skip blank line requirement for idiomatic pairings like desc + task
|
|
154
|
+
# Only applies to block nodes
|
|
155
|
+
return false unless node.block_type? || node.numblock_type?
|
|
156
|
+
return false unless prev_sibling&.send_type?
|
|
157
|
+
return false unless node.send_node
|
|
158
|
+
|
|
159
|
+
prev_method = prev_sibling.method_name
|
|
160
|
+
current_method = node.send_node.method_name
|
|
161
|
+
|
|
162
|
+
paired_methods?(prev_method, current_method)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def paired_methods?(prev_method, current_method)
|
|
166
|
+
# desc + task is idiomatic in rake files
|
|
167
|
+
prev_method == :desc && current_method == :task
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def followed_by_rescue?(node)
|
|
171
|
+
# Don't require blank line before rescue clause
|
|
172
|
+
parent = node.parent
|
|
173
|
+
return false unless parent
|
|
174
|
+
|
|
175
|
+
# Check if parent is a rescue node and next sibling is a resbody
|
|
176
|
+
if parent.rescue_type?
|
|
177
|
+
siblings = parent.children
|
|
178
|
+
index = siblings.index(node)
|
|
179
|
+
return false unless index
|
|
180
|
+
|
|
181
|
+
next_sib = siblings[index + 1]
|
|
182
|
+
return next_sib&.resbody_type?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
false
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def multiline?(node)
|
|
191
|
+
node.loc.first_line != node.loc.last_line
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def block_keyword(node)
|
|
195
|
+
if node.if_type?
|
|
196
|
+
node.keyword
|
|
197
|
+
elsif node.case_type? || node.case_match_type?
|
|
198
|
+
"case"
|
|
199
|
+
elsif node.block_type? || node.numblock_type?
|
|
200
|
+
"do...end"
|
|
201
|
+
else
|
|
202
|
+
"block"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def check_empty_line_before(node)
|
|
207
|
+
return if first_child_of_parent?(node)
|
|
208
|
+
|
|
209
|
+
prev_sibling = previous_sibling(node)
|
|
210
|
+
return unless prev_sibling
|
|
211
|
+
return if prev_sibling.is_a?(Symbol) # Skip non-node siblings
|
|
212
|
+
return if empty_line_between?(prev_sibling, node)
|
|
213
|
+
return if comment_line_before?(node)
|
|
214
|
+
return if preceded_by_paired_method?(node, prev_sibling)
|
|
215
|
+
|
|
216
|
+
keyword = block_keyword(node)
|
|
217
|
+
|
|
218
|
+
add_offense(node, message: format(MSG_BEFORE, keyword: keyword)) do |corrector|
|
|
219
|
+
# Insert newline after the previous line's end
|
|
220
|
+
corrector.insert_before(
|
|
221
|
+
node.source_range.with(
|
|
222
|
+
begin_pos: node.source_range.begin_pos - node.loc.column,
|
|
223
|
+
),
|
|
224
|
+
"\n",
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def check_empty_line_after(node)
|
|
230
|
+
return if last_child_of_parent?(node)
|
|
231
|
+
return if followed_by_rescue?(node)
|
|
232
|
+
|
|
233
|
+
next_sibling = next_sibling(node)
|
|
234
|
+
return unless next_sibling
|
|
235
|
+
return if next_sibling.is_a?(Symbol) # Skip non-node siblings
|
|
236
|
+
return if empty_line_between?(node, next_sibling)
|
|
237
|
+
return if comment_line_after?(node)
|
|
238
|
+
|
|
239
|
+
keyword = block_keyword(node)
|
|
240
|
+
|
|
241
|
+
add_offense(node.loc.end, message: format(MSG_AFTER, keyword: keyword)) do |corrector|
|
|
242
|
+
corrector.insert_after(node, "\n")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def first_child_of_parent?(node)
|
|
247
|
+
parent = node.parent
|
|
248
|
+
return true unless parent
|
|
249
|
+
|
|
250
|
+
# For block/def/class bodies, check within the body only
|
|
251
|
+
return true if only_child_in_body?(node, parent)
|
|
252
|
+
|
|
253
|
+
siblings = body_siblings(parent)
|
|
254
|
+
siblings.first == node
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def last_child_of_parent?(node)
|
|
258
|
+
parent = node.parent
|
|
259
|
+
return true unless parent
|
|
260
|
+
|
|
261
|
+
# For block/def/class bodies, check within the body only
|
|
262
|
+
return true if only_child_in_body?(node, parent)
|
|
263
|
+
|
|
264
|
+
siblings = body_siblings(parent)
|
|
265
|
+
siblings.last == node
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def only_child_in_body?(node, parent)
|
|
269
|
+
# When the node IS the body (not wrapped in begin), it's the only child
|
|
270
|
+
return true if parent.type == :block && parent.body == node
|
|
271
|
+
return true if parent.type == :def && parent.body == node
|
|
272
|
+
return true if parent.type == :defs && parent.body == node
|
|
273
|
+
return true if parent.type == :resbody && parent.body == node
|
|
274
|
+
|
|
275
|
+
# For if/unless, check if node is the only thing in the if-branch or else-branch
|
|
276
|
+
if parent.type == :if
|
|
277
|
+
return true if parent.if_branch == node
|
|
278
|
+
return true if parent.else_branch == node
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# For case/when
|
|
282
|
+
return true if parent.type == :when && parent.body == node
|
|
283
|
+
return true if parent.type == :case && parent.else_branch == node
|
|
284
|
+
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def body_siblings(parent)
|
|
289
|
+
# For begin nodes, all children are body siblings
|
|
290
|
+
# For other nodes, filter to valid siblings
|
|
291
|
+
parent.children.select { |c| valid_sibling?(c) }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def previous_sibling(node)
|
|
295
|
+
parent = node.parent
|
|
296
|
+
return unless parent
|
|
297
|
+
|
|
298
|
+
siblings = parent.children
|
|
299
|
+
index = siblings.index(node)
|
|
300
|
+
return unless index&.positive?
|
|
301
|
+
|
|
302
|
+
# Find previous node sibling (skip non-nodes and nodes without location)
|
|
303
|
+
(index - 1).downto(0) do |i|
|
|
304
|
+
sibling = siblings[i]
|
|
305
|
+
return sibling if valid_sibling?(sibling)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def next_sibling(node)
|
|
312
|
+
parent = node.parent
|
|
313
|
+
return unless parent
|
|
314
|
+
|
|
315
|
+
siblings = parent.children
|
|
316
|
+
index = siblings.index(node)
|
|
317
|
+
return unless index
|
|
318
|
+
|
|
319
|
+
# Find next node sibling (skip non-nodes and nodes without location)
|
|
320
|
+
((index + 1)...siblings.size).each do |i|
|
|
321
|
+
sibling = siblings[i]
|
|
322
|
+
return sibling if valid_sibling?(sibling)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def valid_sibling?(sibling)
|
|
329
|
+
sibling.is_a?(RuboCop::AST::Node) &&
|
|
330
|
+
sibling.loc&.expression
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def empty_line_between?(node1, node2)
|
|
334
|
+
return true unless node1.loc&.expression && node2.loc&.expression
|
|
335
|
+
|
|
336
|
+
line1 = node1.loc.last_line
|
|
337
|
+
line2 = node2.loc.first_line
|
|
338
|
+
|
|
339
|
+
# There's an empty line if there's more than 1 line between them
|
|
340
|
+
(line2 - line1) > 1
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def comment_line_before?(node)
|
|
344
|
+
line_before = node.loc.first_line - 1
|
|
345
|
+
return false if line_before < 1
|
|
346
|
+
|
|
347
|
+
processed_source.comments.any? { |c| c.loc.line == line_before }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def comment_line_after?(node)
|
|
351
|
+
line_after = node.loc.last_line + 1
|
|
352
|
+
processed_source.comments.any? { |c| c.loc.line == line_after }
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sane
|
|
6
|
+
# Prohibits calling methods directly after `end`.
|
|
7
|
+
#
|
|
8
|
+
# This cop detects method calls chained directly on blocks, conditionals,
|
|
9
|
+
# and other structures that end with `end`. Such code is harder to read
|
|
10
|
+
# and should be refactored to assign the result to a variable first.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# if condition
|
|
15
|
+
# value
|
|
16
|
+
# end.foo
|
|
17
|
+
#
|
|
18
|
+
# # bad
|
|
19
|
+
# if condition
|
|
20
|
+
# value
|
|
21
|
+
# end&.foo
|
|
22
|
+
#
|
|
23
|
+
# # bad
|
|
24
|
+
# array.map do |item|
|
|
25
|
+
# transform(item)
|
|
26
|
+
# end.compact
|
|
27
|
+
#
|
|
28
|
+
# # good
|
|
29
|
+
# result = if condition
|
|
30
|
+
# value
|
|
31
|
+
# end
|
|
32
|
+
# result.foo
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# result = array.map do |item|
|
|
36
|
+
# transform(item)
|
|
37
|
+
# end
|
|
38
|
+
# result.compact
|
|
39
|
+
#
|
|
40
|
+
class NoMethodCallAfterEnd < Base
|
|
41
|
+
MSG = "Do not call methods directly after `end`."
|
|
42
|
+
|
|
43
|
+
END_KEYWORD_NODES = [
|
|
44
|
+
:if, :case, :case_match, :while, :until, :for,
|
|
45
|
+
:kwbegin, :block, :numblock,
|
|
46
|
+
:def, :defs, :class, :module, :sclass,
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
BLOCK_NODES = [:block, :numblock].freeze
|
|
50
|
+
|
|
51
|
+
def on_send(node)
|
|
52
|
+
check_method_call_after_end(node)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def on_csend(node)
|
|
56
|
+
check_method_call_after_end(node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def check_method_call_after_end(node)
|
|
62
|
+
receiver = node.receiver
|
|
63
|
+
return unless receiver
|
|
64
|
+
return unless ends_with_end_keyword?(receiver)
|
|
65
|
+
|
|
66
|
+
add_offense(node.loc.dot || node.loc.selector)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ends_with_end_keyword?(node)
|
|
70
|
+
return false unless END_KEYWORD_NODES.include?(node.type)
|
|
71
|
+
return !node.braces? if BLOCK_NODES.include?(node.type)
|
|
72
|
+
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sane/disallow_methods"
|
|
4
|
+
require_relative "sane/conditional_assignment_allow_ternary"
|
|
5
|
+
require_relative "sane/empty_line_before_comment"
|
|
6
|
+
require_relative "sane/empty_lines_around_multiline_block"
|
|
7
|
+
require_relative "sane/no_method_call_after_end"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Sane
|
|
7
|
+
# A plugin that integrates RuboCop Sane with RuboCop's plugin system.
|
|
8
|
+
class Plugin < LintRoller::Plugin
|
|
9
|
+
def about
|
|
10
|
+
LintRoller::About.new(
|
|
11
|
+
name: "rubocop-sane",
|
|
12
|
+
version: VERSION,
|
|
13
|
+
homepage: "https://github.com/akodkod/rubocop-sane",
|
|
14
|
+
description: "Sane RuboCop cops for modern Ruby development.",
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def supported?(context)
|
|
19
|
+
context.engine == :rubocop
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rules(_context)
|
|
23
|
+
LintRoller::Rules.new(
|
|
24
|
+
type: :path,
|
|
25
|
+
config_format: :rubocop,
|
|
26
|
+
value: RuboCop::Sane::CONFIG_DEFAULT,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/rubocop/sane.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
# RuboCop Sane project namespace
|
|
5
|
+
module Sane
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
|
|
9
|
+
CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/rubocop-sane.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubocop-sane
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Kodkod
|
|
8
|
+
bindir: exe
|
|
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.48.0
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 1.48.0
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
46
|
+
description: A collection of RuboCop cops that enforce sensible coding conventions
|
|
47
|
+
email:
|
|
48
|
+
- 678665+akodkod@users.noreply.github.com
|
|
49
|
+
executables: []
|
|
50
|
+
extensions: []
|
|
51
|
+
extra_rdoc_files: []
|
|
52
|
+
files:
|
|
53
|
+
- ".rspec"
|
|
54
|
+
- ".rubocop.yml"
|
|
55
|
+
- ".vscode/cspell.json"
|
|
56
|
+
- ".vscode/project-words.txt"
|
|
57
|
+
- CHANGELOG.md
|
|
58
|
+
- CLAUDE.md
|
|
59
|
+
- CODE_OF_CONDUCT.md
|
|
60
|
+
- LICENSE.txt
|
|
61
|
+
- README.md
|
|
62
|
+
- Rakefile
|
|
63
|
+
- config/default.yml
|
|
64
|
+
- lib/rubocop-sane.rb
|
|
65
|
+
- lib/rubocop/cop/sane/conditional_assignment_allow_ternary.rb
|
|
66
|
+
- lib/rubocop/cop/sane/disallow_methods.rb
|
|
67
|
+
- lib/rubocop/cop/sane/empty_line_before_comment.rb
|
|
68
|
+
- lib/rubocop/cop/sane/empty_lines_around_multiline_block.rb
|
|
69
|
+
- lib/rubocop/cop/sane/no_method_call_after_end.rb
|
|
70
|
+
- lib/rubocop/cop/sane_cops.rb
|
|
71
|
+
- lib/rubocop/sane.rb
|
|
72
|
+
- lib/rubocop/sane/plugin.rb
|
|
73
|
+
- lib/rubocop/sane/version.rb
|
|
74
|
+
- sig/rubocop/sane.rbs
|
|
75
|
+
homepage: https://github.com/akodkod/rubocop-sane
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
homepage_uri: https://github.com/akodkod/rubocop-sane
|
|
80
|
+
source_code_uri: https://github.com/akodkod/rubocop-sane
|
|
81
|
+
changelog_uri: https://github.com/akodkod/rubocop-sane/blob/main/CHANGELOG.md
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
default_lint_roller_plugin: RuboCop::Sane::Plugin
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 3.2.0
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.6.9
|
|
99
|
+
specification_version: 4
|
|
100
|
+
summary: Sane RuboCop cops for modern Ruby development
|
|
101
|
+
test_files: []
|