├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── console ├── config └── default.yml ├── lib ├── rubocop-ordered_methods.rb └── rubocop │ ├── cop │ ├── alias_method_order_verifier.rb │ ├── correctors │ │ └── ordered_methods_corrector.rb │ ├── layout │ │ └── ordered_methods.rb │ └── qualifier_node_matchers.rb │ └── ordered_methods.rb ├── rubocop-ordered_methods.gemspec └── spec ├── rubocop └── cop │ └── layout │ └── ordered_methods_spec.rb ├── spec_helper.rb └── support └── file_helper.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | push: 8 | branches: 9 | - master 10 | paths-ignore: 11 | - '**.md' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | rspec: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | ruby: 20 | - "2.7" 21 | - "3.0" 22 | - "3.1" 23 | - "3.2" 24 | - "3.3" 25 | - "3.4" 26 | 27 | name: "Ruby ${{ matrix.ruby }}: run rspec" 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "${{ matrix.ruby }}" 33 | bundler-cache: true 34 | - run: bundle exec rspec 35 | 36 | rubocop: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | ruby: 41 | - "2.7" 42 | - "3.0" 43 | - "3.1" 44 | - "3.2" 45 | - "3.3" 46 | - "3.4" 47 | 48 | name: "Ruby ${{ matrix.ruby }}: run rubocop" 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: ruby/setup-ruby@v1 52 | with: 53 | ruby-version: "${{ matrix.ruby }}" 54 | bundler-cache: true 55 | - run: bundle exec rubocop 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | Gemfile.lock 11 | .ruby-version 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: rubocop-ordered_methods 4 | 5 | AllCops: 6 | NewCops: enable 7 | SuggestExtensions: false 8 | TargetRubyVersion: 2.7 9 | 10 | # Subtle, left to author's discretion. In a long method with many guard clauses, 11 | # a blank line may help. But, in a short method, especially with only a single 12 | # guard clause, a blank line can be disruptive. 13 | Layout/EmptyLineAfterGuardClause: 14 | Enabled: false 15 | 16 | Layout/LineEndStringConcatenationIndentation: 17 | EnforcedStyle: indented 18 | 19 | Metrics/BlockLength: 20 | Exclude: 21 | - spec/**/* 22 | 23 | Metrics/MethodLength: 24 | Exclude: 25 | - spec/**/* 26 | 27 | Naming/FileName: 28 | Exclude: 29 | - lib/rubocop-ordered_methods.rb 30 | 31 | # Use the semantic style. If a block has side effects use `do`, and if it is 32 | # pure use `{}`. This style is too nuanced for a linter, so the cop is 33 | # disabled. 34 | Style/BlockDelimiters: 35 | Enabled: false 36 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-01-05 20:47:55 UTC using RuboCop version 1.7.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 11 | Metrics/MethodLength: 12 | Max: 11 13 | 14 | # Offense count: 1 15 | # Cop supports --auto-correct. 16 | Style/IfUnlessModifier: 17 | Exclude: 18 | - 'lib/rubocop/cop/layout/ordered_methods.rb' 19 | 20 | # Offense count: 1 21 | # Cop supports --auto-correct. 22 | Style/RedundantFreeze: 23 | Exclude: 24 | - 'lib/rubocop/cop/layout/ordered_methods.rb' 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.14] - 2025-05-02 10 | 11 | ### Fixed 12 | 13 | - rubocop-ordered_methods ignores outer classes when inspecting files [#19](https://github.com/shanecav84/rubocop-ordered_methods/pull/19). Thanks, @ShkumbinDelija. 14 | 15 | ## [0.13] - 2024-10-01 16 | 17 | ### Fixed 18 | 19 | - Fix clobbering when consecutive nodes are with wrong order [#17](https://github.com/shanecav84/rubocop-ordered_methods/pull/17). Thanks, @Darhazer. 20 | 21 | ## [0.12] - 2024-07-16 22 | 23 | ### Fixed 24 | 25 | - Add MethodQualifiers to the default config [#13](https://github.com/shanecav84/rubocop-ordered_methods/pull/13). Thanks, @Darhazer. 26 | - Fix rubocop 1.65 compatability [#16](https://github.com/shanecav84/rubocop-ordered_methods/pull/16). Thanks, @Darhazer. 27 | 28 | ## [0.11] - 2023-12-19 29 | 30 | ### Fixed 31 | 32 | - Fix handling require at the top of the file, nil nodes, and nodes that aren't of type AST::node ([#15](https://github.com/shanecav84/rubocop-ordered_methods/pull/15)). Thanks @rohitpaulk and @libmartinito 33 | 34 | ## [0.10] - 2021-03-10 35 | 36 | ### Removed 37 | 38 | - Drop support for Ruby 2.4, 2.5, and 2.6 39 | 40 | ### Added 41 | 42 | - Support for custom method qualifiers ([#11](https://github.com/shanecav84/rubocop-ordered_methods/pull/11)). Thanks @Darhazer. 43 | - Setup CI ([#12](https://github.com/shanecav84/rubocop-ordered_methods/pull/12)). Thanks @Darhazer. 44 | 45 | ## [0.9] - 2021-03-10 46 | 47 | ### Added 48 | 49 | - Autocorrection support for Sorbet signatures 50 | 51 | ## [0.8] - 2021-02-01 52 | 53 | ### Fixed 54 | 55 | - Fix NoMethodError and the "\[Correctable\]" label ([#6](https://github.com/shanecav84/rubocop-ordered_methods/pull/6)). Thanks @jaredbeck. 56 | 57 | ## [0.7] - 2021-01-11 58 | 59 | ### Removed 60 | 61 | - Drop Ruby 2.3 support 62 | - Drop support for rubocop < 1.0 63 | 64 | ### Changed 65 | 66 | - Support for rubocop >= 1.0 ([#5](https://github.com/shanecav84/rubocop-ordered_methods/pull/5)). Thanks @jaredbeck. 67 | 68 | ## [0.6] - 2020-03-01 69 | 70 | ### Security 71 | 72 | - Upgrade rake to avoid vulnerability https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8130 73 | - rake is a development dependency for this gem, so shouldn't have been a risk for production 74 | 75 | ## [0.5] - 2019-11-05 76 | 77 | ### Removed 78 | 79 | - Drop Ruby 2.2 support 80 | 81 | ### Changed 82 | 83 | - Nonadjacent qualifiers are now autocorrected (#4). Thanks @adamkiczula. 84 | - Cache AST traversals for significant speed up on large files 85 | 86 | ## [0.4] - 2019-06-11 87 | 88 | ### Changed 89 | 90 | - More robust autocorrection of a method and its surroundings (see `Corrector` in the `README`). 91 | 92 | ## [0.3] - 2019-02-17 93 | 94 | ### Added 95 | 96 | - Configuration defaults 97 | 98 | ## [0.2] - 2019-02-17 99 | 100 | ### Added 101 | 102 | - Autocorrector 103 | 104 | ## [0.1] - 2019-02-17 105 | 106 | Initial release. 107 | 108 | [Unreleased]: https://github.com/shanecav84/rubocop-ordered_methods/compare/v0.3...HEAD 109 | [0.3]: https://github.com/shanecav84/rubocop-ordered_methods/compare/v0.2...v0.3 110 | [0.2]: https://github.com/shanecav84/rubocop-ordered_methods/compare/v0.1...v0.2 111 | [0.1]: https://github.com/shanecav84/rubocop-ordered_methods/releases/tag/v0.1 112 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at shane@shanecav.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'bundler' 6 | gem 'byebug' 7 | gem 'rake', '~> 12.3.3' 8 | gem 'rspec', '~> 3.0' 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Shane Cavanaugh 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/rubocop-ordered_methods.svg)](https://badge.fury.io/rb/rubocop-ordered_methods) 2 | [![Build Status](https://github.com/shanecav84/rubocop-ordered_methods/actions/workflows/main.yml/badge.svg)](https://github.com/shanecav84/rubocop-ordered_methods/actions) 3 | 4 | # RuboCop OrderedMethods 5 | 6 | Check that methods are defined alphabetically per access modifier block (class, 7 | public, private, protected). Includes [autocorrection](#corrector). 8 | 9 | ```ruby 10 | # bad 11 | def self.b_class; end 12 | def self.a_class; end 13 | 14 | def b_public; end 15 | def a_public; end 16 | 17 | private 18 | 19 | def b_private; end 20 | def a_private; end 21 | 22 | # good 23 | def self.a_class; end 24 | def self.b_class; end 25 | 26 | def a_public; end 27 | def b_public; end 28 | 29 | private 30 | 31 | def a_private; end 32 | def b_private; end 33 | ``` 34 | 35 | ## Installation 36 | 37 | Add this line to your application's Gemfile: 38 | 39 | ```ruby 40 | gem 'rubocop-ordered_methods' 41 | ``` 42 | 43 | And then execute: 44 | 45 | $ bundle 46 | 47 | Or install it yourself as: 48 | 49 | $ gem install rubocop-ordered_methods 50 | 51 | ## Usage 52 | 53 | You need to tell RuboCop to load the OrderedMethods extension. There are two 54 | ways to do this: 55 | 56 | ### RuboCop configuration file 57 | 58 | Put this into your `.rubocop.yml`. 59 | 60 | ``` 61 | require: rubocop-ordered_methods 62 | ``` 63 | 64 | Now you can run `rubocop` and it will automatically load the RuboCop OrderedMethods 65 | cops together with the standard cops. 66 | 67 | ### Command line 68 | 69 | ```bash 70 | rubocop --require rubocop-ordered_methods 71 | ``` 72 | 73 | ### Configurable attributes 74 | 75 | Name | Default value | Configurable values 76 | --- | --- | --- 77 | EnforcedStyle | `'alphabetical'` | `'alphabetical'` 78 | IgnoredMethods | `['initialize']` | Array 79 | MethodQualifiers | `[]` | Array 80 | Signature | `nil` | `'sorbet'`, `nil` 81 | 82 | #### Example 83 | 84 | ``` 85 | # .rubocop.yml 86 | Layout/OrderedMethods: 87 | EnforcedStyle: alphabetical 88 | IgnoredMethods: 89 | - initialize 90 | MethodQualifiers: 91 | - memoize 92 | Signature: sorbet 93 | ``` 94 | 95 | ### Corrector 96 | 97 | The corrector will attempt to order methods based on the `EnforcedStyle`. It attempts to 98 | include surrounding comments and the qualifiers (e.g., aliases) listed in 99 | `::RuboCop::Cop::OrderedMethodsCorrector::QUALIFIERS`. The following (monstrous) 100 | source is able to be correctly ordered: 101 | 102 | ```ruby 103 | # Long 104 | # Preceding 105 | # Comment 106 | # class_b 107 | def self.class_b; end 108 | private_class_method :class_b 109 | 110 | def self.class_a; end 111 | # Long 112 | # Succeeding 113 | # Comment 114 | # class_a 115 | public_class_method :class_a 116 | 117 | # Preceding comment for instance_b 118 | def instance_b; end 119 | # Long 120 | # Succeeding 121 | # Comment 122 | # instance_b 123 | alias_method :orig_instance_b, :instance_b 124 | module_function :instance_b 125 | private :instance_b 126 | protected :instance_b 127 | public :instance_b 128 | 129 | # Long 130 | # Preceding 131 | # Comment 132 | # instance_a 133 | def instance_a; end 134 | # Succeeding comment for instance_a 135 | alias :new_instance_a :instance_a 136 | alias_method :orig_instance_a, :instance_a 137 | module_function :instance_a 138 | private :instance_a 139 | protected :instance_a 140 | public :instance_a 141 | ``` 142 | 143 | #### Method qualifiers 144 | Some gems (like `memery`, `memoist`, etc.) provide a DSL that modifies the method (e.g. for memoization). 145 | Those DSL methods can be added to the `MethodQualifiers` configuration, and they will be respected. 146 | 147 | E.g. the following source can be correctly ordered: 148 | ```ruby 149 | def b; end; 150 | memoize def a;end 151 | ``` 152 | 153 | #### Method signatures 154 | 155 | Support for (Sorbet) method signatures was added to the corrector by 156 | [#7](https://github.com/shanecav84/rubocop-ordered_methods/pull/7). 157 | It is off by default due to performance concerns (not yet benchmarked). Enable 158 | with `Signature: sorbet`. 159 | 160 | #### Caveats 161 | 162 | * The corrector will warn and refuse to order a method if it were to be 163 | defined before its alias 164 | * If there's ambiguity about which method a comment or qualifier belongs to, 165 | the corrector might fail to order correctly. For example, in the following, 166 | the corrector would incorrectly order the comment as a comment of `a`: 167 | 168 | ```ruby 169 | def b; end 170 | # Comment b 171 | def a; end 172 | ``` 173 | 174 | ## Project status 175 | 176 | Development is ongoing. There might be extended periods between releases. 177 | The maintenance goal is to add features as needed and to maintain compatibility 178 | with Ruby and rubocop updates. 179 | 180 | ## Development 181 | 182 | ### Setup 183 | 184 | ```bash 185 | bundle install 186 | bundle exec rake 187 | ``` 188 | 189 | ## Contributing 190 | 191 | Bug reports and pull requests are welcome on GitHub at 192 | https://github.com/shanecav84/rubocop-ordered_methods. This project is intended 193 | to be a safe, welcoming space for collaboration, and contributors are expected 194 | to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of 195 | conduct. 196 | 197 | ## License 198 | 199 | The gem is available as open source under the terms of the 200 | [MIT License](https://opensource.org/licenses/MIT). 201 | 202 | ## Code of Conduct 203 | 204 | Everyone interacting in the RuboCop OrderedMethods project’s codebases, issue 205 | trackers, chat rooms and mailing lists is expected to follow the 206 | [code of conduct](https://github.com/shanecav84/rubocop-ordered_methods/blob/master/CODE_OF_CONDUCT.md). 207 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rubocop/rake_task' 5 | require 'rspec/core/rake_task' 6 | 7 | RuboCop::RakeTask.new 8 | RSpec::Core::RakeTask.new(:spec) 9 | 10 | task default: %i[rubocop spec] 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'rubocop/ordered_methods' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /config/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Layout/OrderedMethods: 3 | Enabled: true 4 | EnforcedStyle: 'alphabetical' 5 | IgnoredMethods: 6 | - initialize 7 | MethodQualifiers: [] 8 | Signature: ~ 9 | -------------------------------------------------------------------------------- /lib/rubocop-ordered_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubocop' 4 | require_relative 'rubocop/ordered_methods' 5 | require_relative 'rubocop/cop/layout/ordered_methods' 6 | require_relative 'rubocop/cop/correctors/ordered_methods_corrector' 7 | 8 | RuboCop::OrderedMethods.inject_defaults! 9 | -------------------------------------------------------------------------------- /lib/rubocop/cop/alias_method_order_verifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'qualifier_node_matchers' 4 | 5 | module RuboCop 6 | module Cop 7 | # This verifies a method is defined before its alias 8 | class AliasMethodOrderVerifier 9 | class << self 10 | include IgnoredNode 11 | include QualifierNodeMatchers 12 | 13 | # Disable cop for freezing on Ruby 2.2 14 | # rubocop:disable Style/RedundantFreeze 15 | ALIAS_BEFORE_METHOD_WARNING_FMT = "Won't reorder " \ 16 | '%s and %s because ' \ 17 | 'alias for %s would be declared before ' \ 18 | 'its method definition.'.freeze 19 | # rubocop:enable Style/RedundantFreeze 20 | 21 | # rubocop:disable Style/GuardClause 22 | def verify!(current_node, previous_node) 23 | if moving_after_alias?(current_node, previous_node) 24 | ignore_node(current_node) 25 | raise_warning!(current_node.method_name, previous_node.method_name) 26 | end 27 | if moving_after_alias?(previous_node, current_node) 28 | ignore_node(previous_node) 29 | raise_warning!(previous_node.method_name, current_node.method_name) 30 | end 31 | end 32 | # rubocop:enable Style/GuardClause 33 | 34 | private 35 | 36 | def find_aliases(current_node, siblings) 37 | siblings.select do |sibling| 38 | (alias?(sibling) || alias_method?(sibling)) == 39 | current_node.method_name 40 | end 41 | end 42 | 43 | # We don't want a method to be defined after its alias 44 | def moving_after_alias?(current_node, previous_node) 45 | siblings = current_node.parent.children 46 | current_node_aliases = find_aliases(current_node, siblings) 47 | filter = current_node_aliases.delete_if do |cna| 48 | cna.sibling_index > current_node.sibling_index 49 | end 50 | return false if filter.empty? 51 | 52 | current_node_aliases.any? do |cna| 53 | previous_node.sibling_index > cna.sibling_index 54 | end 55 | end 56 | 57 | def raise_warning!(first_method_name, second_method_name) 58 | raise Warning, format( 59 | ALIAS_BEFORE_METHOD_WARNING_FMT, 60 | first_method_name: first_method_name, 61 | second_method_name: second_method_name 62 | ) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/rubocop/cop/correctors/ordered_methods_corrector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../layout/ordered_methods' 4 | require_relative '../alias_method_order_verifier' 5 | require_relative '../qualifier_node_matchers' 6 | 7 | module RuboCop 8 | module Cop 9 | # This auto-corrects method order 10 | class OrderedMethodsCorrector 11 | include QualifierNodeMatchers 12 | 13 | # @param cop_config ::RuboCop::Config 14 | def initialize(comment_locations, siblings, cop_config) 15 | @comment_locations = comment_locations 16 | @siblings = siblings 17 | @cop_config = cop_config 18 | end 19 | 20 | def correct(node, previous_node, corrector) 21 | AliasMethodOrderVerifier.verify!(node, previous_node) 22 | current_range = join_surroundings(node) 23 | previous_range = join_surroundings(previous_node) 24 | corrector.swap(current_range, previous_range) 25 | end 26 | 27 | private 28 | 29 | def find_last_qualifier_index(node) 30 | preceding_qualifier_index = node.sibling_index 31 | last_qualifier_index = @siblings.length - 1 32 | while preceding_qualifier_index < last_qualifier_index 33 | break if found_qualifier?(node, @siblings[last_qualifier_index]) 34 | 35 | last_qualifier_index -= 1 36 | end 37 | 38 | last_qualifier_index 39 | end 40 | 41 | def found_qualifier?(node, next_sibling) 42 | return false if next_sibling.nil? 43 | 44 | (qualifier?(next_sibling) || alias?(next_sibling)) == node.method_name 45 | end 46 | 47 | # @param node RuboCop::AST::DefNode 48 | # @param source_range Parser::Source::Range 49 | # @return Parser::Source::Range 50 | def join_comments(node, source_range) 51 | @comment_locations[node].each do |comment| 52 | source_range = source_range.join(comment.loc.expression) 53 | end 54 | source_range 55 | end 56 | 57 | # @param node RuboCop::AST::DefNode 58 | # @param source_range Parser::Source::Range 59 | # @return Parser::Source::Range 60 | def join_modifiers_and_aliases(node, source_range) 61 | preceding_qualifier_index = node.sibling_index 62 | last_qualifier_index = find_last_qualifier_index(node) 63 | while preceding_qualifier_index < last_qualifier_index 64 | source_range = source_range.join( 65 | @siblings[preceding_qualifier_index + 1].source_range 66 | ) 67 | preceding_qualifier_index += 1 68 | end 69 | source_range 70 | end 71 | 72 | # @param node RuboCop::AST::DefNode 73 | # @param source_range Parser::Source::Range 74 | # @return Parser::Source::Range 75 | def join_signature(node, source_range) 76 | sib = node.left_sibling 77 | if signature?(sib) 78 | # If there is a comment directly above the sig, first calculate the 79 | # range that covers both. 80 | with_comment = join_comments(sib, sib.source_range) 81 | source_range.join(with_comment) 82 | else 83 | source_range 84 | end 85 | end 86 | 87 | def join_signature? 88 | @cop_config['Signature'] == 'sorbet' 89 | end 90 | 91 | # @param node RuboCop::AST::DefNode 92 | # @return Parser::Source::Range 93 | def join_surroundings(node) 94 | with_modifiers_and_aliases = join_modifiers_and_aliases( 95 | node, 96 | node.source_range 97 | ) 98 | with_comments = join_comments(node, with_modifiers_and_aliases) 99 | if join_signature? 100 | join_signature(node, with_comments) 101 | else 102 | with_comments 103 | end 104 | end 105 | 106 | # https://sorbet.org/docs/sigs 107 | # @param node RuboCop::AST::Node 108 | def signature?(node) 109 | return false unless node&.type == :block 110 | child = node.children.first 111 | child&.type == :send && child.method_name == :sig 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/rubocop/cop/layout/ordered_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module Layout 6 | # @example EnforcedStyle: alphabetical (default) 7 | # # Check that methods are defined alphabetically. 8 | # 9 | # # bad 10 | # def self.b; end 11 | # def self.a; end 12 | # 13 | # def b; end 14 | # def a; end 15 | # 16 | # private 17 | # 18 | # def d; end 19 | # def c; end 20 | # 21 | # # good 22 | # def self.a; end 23 | # def self.b; end 24 | # 25 | # def a; end 26 | # def b; end 27 | # 28 | # private 29 | # 30 | # def c; end 31 | # def d; end 32 | class OrderedMethods < Base 33 | extend AutoCorrector 34 | include AllowedMethods 35 | include RangeHelp 36 | 37 | COMPARISONS = { 38 | 'alphabetical' => lambda do |left_node, right_node| 39 | (method_name(left_node) <=> method_name(right_node)) != 1 40 | end 41 | }.freeze 42 | ERR_INVALID_COMPARISON = 'Invalid "Comparison" config for ' \ 43 | "#{cop_name}. Expected one of: #{COMPARISONS.keys.join(', ')}".freeze 44 | 45 | def self.method_name(node) 46 | return node.method_name unless node.send_type? 47 | 48 | node.first_argument.method_name 49 | end 50 | 51 | def on_begin(node) 52 | check(node) 53 | 54 | node.each_descendant(:class, :module) { |descendant| check(descendant) } 55 | end 56 | 57 | private 58 | 59 | def access_modified?(node, is_class_method_block) 60 | (node.defs_type? && !is_class_method_block) || 61 | (node.def_type? && is_class_method_block) || 62 | (node.send_type? && node.bare_access_modifier?) 63 | end 64 | 65 | def check(start_node) 66 | consecutive_methods(start_node.children) do |previous, current| 67 | next if ordered?(previous, current) 68 | 69 | add_offense(current, message: message) do |corrector| 70 | next if part_of_ignored_node?(previous) 71 | 72 | OrderedMethodsCorrector.new( 73 | processed_source.ast_with_comments, start_node.children, cop_config 74 | ).correct(current, previous, corrector) 75 | 76 | ignore_node(current) 77 | end 78 | end 79 | end 80 | 81 | # We disable `Style/ExplicitBlockArgument` for performance. See 82 | # https://github.com/shanecav84/rubocop-ordered_methods/pull/5#pullrequestreview-562957146 83 | # rubocop:disable Style/ExplicitBlockArgument 84 | def consecutive_methods(nodes) 85 | filtered = filter_relevant_nodes(nodes) 86 | filtered_and_grouped = group_methods_by_access_modifier(filtered) 87 | filtered_and_grouped.each do |method_group| 88 | method_group.each_cons(2) do |left_method, right_method| 89 | yield left_method, right_method 90 | end 91 | end 92 | end 93 | # rubocop:enable Style/ExplicitBlockArgument 94 | 95 | def filter_relevant_nodes(nodes) 96 | nodes.compact.select do |node| 97 | next unless node.is_a?(Parser::AST::Node) 98 | relevant_node?(node) || (node.send_type? && qualifier_macro?(node)) 99 | end 100 | end 101 | 102 | # Group methods by the access modifier block they are declared in. 103 | # Multiple blocks of the same modifier will have their methods grouped 104 | # separately; for example, the following would be separated into two 105 | # groups: 106 | # private 107 | # def a; end 108 | # private 109 | # def b; end 110 | def group_methods_by_access_modifier(nodes) 111 | is_class_method_block = false 112 | 113 | nodes.each_with_object([[]]) do |node, grouped_methods| 114 | if access_modified?(node, is_class_method_block) 115 | grouped_methods << [] 116 | end 117 | is_class_method_block = node.defs_type? 118 | next if node.send_type? && node.bare_access_modifier? 119 | 120 | grouped_methods.last << node 121 | end 122 | end 123 | 124 | def message 125 | "Methods should be sorted in #{cop_config['EnforcedStyle']} order." 126 | end 127 | 128 | def ordered?(left_method, right_method) 129 | comparison = COMPARISONS[cop_config['EnforcedStyle']] 130 | raise Error, ERR_INVALID_COMPARISON if comparison.nil? 131 | 132 | comparison.call(left_method, right_method) 133 | end 134 | 135 | def qualifier_macro?(node) 136 | return true if node.bare_access_modifier? 137 | 138 | cop_config['MethodQualifiers'].to_a.include?(node.method_name.to_s) && 139 | relevant_node?(node.first_argument) 140 | end 141 | 142 | def relevant_node?(node) 143 | (node.defs_type? || node.def_type?) && !allowed_method?(node.method_name) 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/rubocop/cop/qualifier_node_matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | # defines matchers for qualifier nodes 6 | module QualifierNodeMatchers 7 | extend NodePattern::Macros 8 | 9 | QUALIFIERS = %i[ 10 | alias_method 11 | module_function 12 | private_class_method 13 | public_class_method 14 | private 15 | protected 16 | public 17 | ].freeze 18 | 19 | def_node_matcher :alias?, '(:alias ... (sym $_method_name))' 20 | def_node_matcher :alias_method?, 21 | '(send nil? {:alias_method} ... (sym $_method_name))' 22 | def_node_matcher :qualifier?, <<-PATTERN 23 | (send nil? #method_qualifier? ... (sym $_method_name)) 24 | PATTERN 25 | 26 | def method_qualifier?(name) 27 | qualifiers.include?(name) 28 | end 29 | 30 | def qualifiers 31 | @qualifiers ||= QUALIFIERS + @cop_config['MethodQualifiers'].to_a.map(&:to_sym) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rubocop/ordered_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | # Our namespace 5 | module OrderedMethods 6 | PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze 7 | CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze 8 | 9 | def self.inject_defaults! 10 | path = CONFIG_DEFAULT.to_s 11 | hash = ConfigLoader.send(:load_yaml_configuration, path) 12 | config = Config.new(hash, path) 13 | puts "configuration from #{path}" if ConfigLoader.debug? 14 | config = ConfigLoader.merge_with_default(config, path) 15 | ConfigLoader.instance_variable_set(:@default_configuration, config) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /rubocop-ordered_methods.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rubocop-ordered_methods' 8 | spec.version = '0.14' 9 | spec.authors = ['Shane Cavanaugh'] 10 | spec.email = ['shane@shanecav.net'] 11 | 12 | spec.summary = 'Checks that methods are ordered alphabetically.' 13 | spec.homepage = 'https://github.com/shanecav84/rubocop-ordered_methods' 14 | spec.license = 'MIT' 15 | spec.required_ruby_version = '>= 2.7' 16 | 17 | # Specify which files should be added to the gem when it is released. 18 | # The `git ls-files -z` loads the files in the RubyGem that have been added 19 | # into git. 20 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 21 | `git ls-files -z`.split("\x0").reject do |f| 22 | f.match(%r{^(test|spec|features)/}) 23 | end 24 | end 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ['lib'] 28 | 29 | spec.add_dependency 'rubocop', '>= 1.0' 30 | 31 | spec.metadata['rubygems_mfa_required'] = 'true' 32 | end 33 | -------------------------------------------------------------------------------- /spec/rubocop/cop/layout/ordered_methods_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe RuboCop::Cop::Layout::OrderedMethods do 6 | subject(:cop) { described_class.new(config) } 7 | 8 | let(:config) { 9 | RuboCop::Config.new( 10 | 'Layout/OrderedMethods' => { 11 | 'IgnoredMethods' => %w[initialize], 12 | 'EnforcedStyle' => enforced_style 13 | }.merge(cop_config) 14 | ) 15 | } 16 | let(:cop_config) { {} } 17 | let(:enforced_style) { 'alphabetical' } 18 | 19 | it 'registers an offense when methods are not in alphabetical order' do 20 | expect_offense(<<~RUBY) 21 | class Foo 22 | def self.class_b; end 23 | def self.class_a; end 24 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 25 | 26 | def instance_b; end 27 | def instance_a; end 28 | ^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 29 | 30 | module_function 31 | 32 | def module_function_b; end 33 | def module_function_a; end 34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 35 | 36 | private 37 | 38 | def private_b; end 39 | def private_a; end 40 | ^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 41 | 42 | public 43 | 44 | def public_b; end 45 | def public_a; end 46 | ^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 47 | 48 | private 49 | 50 | def private_d; end 51 | def private_c; end 52 | ^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 53 | 54 | protected 55 | 56 | def protected_b; end 57 | def protected_a; end 58 | ^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 59 | 60 | def self.class_d; end 61 | def self.class_c; end 62 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 63 | 64 | def instance_d; end 65 | def instance_c; end 66 | ^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 67 | end 68 | RUBY 69 | end 70 | 71 | it 'registers an offense when require is present at the top of the file (regression)' do 72 | expect_offense(<<~RUBY) 73 | require "date" 74 | 75 | class Foo 76 | def self.class_b; end 77 | def self.class_a; end 78 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 79 | end 80 | RUBY 81 | end 82 | 83 | it 'does not register an offense when methods are in alphabetical order' do 84 | expect_no_offenses(<<~RUBY) 85 | def a; end 86 | def b; end 87 | RUBY 88 | end 89 | 90 | it 'does not register an offense when there are nodes in the class but no methods' do 91 | expect_no_offenses(<<~RUBY) 92 | require "../app/lib/bar" 93 | 94 | class Foo 95 | include Bar 96 | end 97 | RUBY 98 | end 99 | 100 | it 'auto-corrects consecutive offenses' do 101 | expect_offense(<<~RUBY) 102 | require "date" 103 | 104 | class Foo 105 | def self.class_c; end 106 | 107 | def self.class_b; end 108 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 109 | 110 | def self.class_a; end 111 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 112 | end 113 | RUBY 114 | 115 | # first auto-correction pass 116 | expect_correction(<<~RUBY) 117 | require "date" 118 | 119 | class Foo 120 | def self.class_b; end 121 | 122 | def self.class_c; end 123 | 124 | def self.class_a; end 125 | end 126 | RUBY 127 | end 128 | 129 | it 'autocorrects methods that are not in alphabetical order' do 130 | new_source = autocorrect_source_file(<<~RUBY) 131 | class Foo 132 | # Comment class_b 133 | def self.class_b; end 134 | def self.class_a; end 135 | 136 | # Comment instance_b 137 | def instance_b; end 138 | def instance_a; end 139 | alias foo instance_a 140 | alias_method :foo, :instance_a 141 | 142 | module_function 143 | 144 | # Comment module_function_b 145 | def module_function_b; end 146 | def module_function_a; end 147 | 148 | private 149 | 150 | # Comment private_b 151 | def private_b; end 152 | def private_a; end 153 | 154 | private 155 | 156 | # Comment private_d 157 | def private_d; end 158 | def private_c; end 159 | 160 | protected 161 | 162 | # Comment protected_b 163 | def protected_b; end 164 | def protected_a; end 165 | 166 | public 167 | 168 | # Comment public_b 169 | def public_b; end 170 | def public_a; end 171 | 172 | # Comment class_d 173 | def self.class_d; end 174 | def self.class_c; end 175 | 176 | # Comment instance_d 177 | def instance_d; end 178 | def instance_c; end 179 | end 180 | RUBY 181 | 182 | expect(new_source).to eq(<<~RUBY) 183 | class Foo 184 | def self.class_a; end 185 | # Comment class_b 186 | def self.class_b; end 187 | 188 | def instance_a; end 189 | alias foo instance_a 190 | alias_method :foo, :instance_a 191 | # Comment instance_b 192 | def instance_b; end 193 | 194 | module_function 195 | 196 | def module_function_a; end 197 | # Comment module_function_b 198 | def module_function_b; end 199 | 200 | private 201 | 202 | def private_a; end 203 | # Comment private_b 204 | def private_b; end 205 | 206 | private 207 | 208 | def private_c; end 209 | # Comment private_d 210 | def private_d; end 211 | 212 | protected 213 | 214 | def protected_a; end 215 | # Comment protected_b 216 | def protected_b; end 217 | 218 | public 219 | 220 | def public_a; end 221 | # Comment public_b 222 | def public_b; end 223 | 224 | def self.class_c; end 225 | # Comment class_d 226 | def self.class_d; end 227 | 228 | def instance_c; end 229 | # Comment instance_d 230 | def instance_d; end 231 | end 232 | RUBY 233 | end 234 | 235 | it 'autocorrects with comments and modifiers' do 236 | source = <<-RUBY 237 | # Long 238 | # Preceding 239 | # Comment 240 | # class_b 241 | def self.class_b; end 242 | private_class_method :class_b 243 | 244 | def self.class_a; end 245 | # Long 246 | # Succeeding 247 | # Comment 248 | # class_a 249 | public_class_method :class_a 250 | 251 | # Preceding comment for instance_b 252 | def instance_b; end 253 | # Long 254 | # Succeeding 255 | # Comment 256 | # instance_b 257 | alias_method :orig_instance_b, :instance_b 258 | module_function :instance_b 259 | private :instance_b 260 | protected :instance_b 261 | public :instance_b 262 | 263 | # Long 264 | # Preceding 265 | # Comment 266 | # instance_a 267 | def instance_a; end 268 | # Succeeding comment for instance_a 269 | alias :new_instance_a :instance_a 270 | alias_method :orig_instance_a, :instance_a 271 | module_function :instance_a 272 | private :instance_a 273 | protected :instance_a 274 | public :instance_a 275 | RUBY 276 | 277 | expect(autocorrect_source_file(source)).to eq(<<-RUBY) 278 | def self.class_a; end 279 | # Long 280 | # Succeeding 281 | # Comment 282 | # class_a 283 | public_class_method :class_a 284 | 285 | # Long 286 | # Preceding 287 | # Comment 288 | # class_b 289 | def self.class_b; end 290 | private_class_method :class_b 291 | 292 | # Long 293 | # Preceding 294 | # Comment 295 | # instance_a 296 | def instance_a; end 297 | # Succeeding comment for instance_a 298 | alias :new_instance_a :instance_a 299 | alias_method :orig_instance_a, :instance_a 300 | module_function :instance_a 301 | private :instance_a 302 | protected :instance_a 303 | public :instance_a 304 | 305 | # Preceding comment for instance_b 306 | def instance_b; end 307 | # Long 308 | # Succeeding 309 | # Comment 310 | # instance_b 311 | alias_method :orig_instance_b, :instance_b 312 | module_function :instance_b 313 | private :instance_b 314 | protected :instance_b 315 | public :instance_b 316 | RUBY 317 | end 318 | 319 | it 'autocorrects when qualifiers are not immediately adjacent' do 320 | source = <<-RUBY 321 | def method_b; end 322 | alias_method :method_from_parent_class_orig, :method_from_parent_class 323 | alias_method :method_from_parent_class, :method_b 324 | 325 | def method_a; end 326 | RUBY 327 | 328 | expect(autocorrect_source_file(source)).to eq(<<-RUBY) 329 | def method_a; end 330 | 331 | def method_b; end 332 | alias_method :method_from_parent_class_orig, :method_from_parent_class 333 | alias_method :method_from_parent_class, :method_b 334 | RUBY 335 | end 336 | 337 | context 'when a class has inner classes/modules' do 338 | let(:expected_offense) do 339 | <<~RUBY 340 | class Parent 341 | def self.parent_b; end 342 | def self.parent_a; end 343 | ^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 344 | 345 | def parent_instance_b; end 346 | def parent_instance_a; end 347 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 348 | 349 | class Child 350 | def self.child_b; end 351 | def self.child_a; end 352 | ^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 353 | 354 | def child_instance_b; end 355 | def child_instance_a; end 356 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 357 | end 358 | 359 | module InnerModule 360 | def self.inner_module_b; end 361 | def self.inner_module_a; end 362 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 363 | 364 | def inner_instance_b; end 365 | def inner_instance_a; end 366 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 367 | end 368 | end 369 | RUBY 370 | end 371 | 372 | let(:expected_correction) do 373 | <<~RUBY 374 | class Parent 375 | def self.parent_a; end 376 | def self.parent_b; end 377 | 378 | def parent_instance_a; end 379 | def parent_instance_b; end 380 | 381 | class Child 382 | def self.child_a; end 383 | def self.child_b; end 384 | 385 | def child_instance_a; end 386 | def child_instance_b; end 387 | end 388 | 389 | module InnerModule 390 | def self.inner_module_a; end 391 | def self.inner_module_b; end 392 | 393 | def inner_instance_a; end 394 | def inner_instance_b; end 395 | end 396 | end 397 | RUBY 398 | end 399 | 400 | it 'registers an offense for methods not in alphabetical order in inner classes and modules' do 401 | expect_offense(expected_offense) 402 | expect_correction(expected_correction) 403 | end 404 | 405 | it 'does not register an offense for methods in alphabetical order in inner classes and modules' do 406 | expect_no_offenses(expected_correction) 407 | end 408 | end 409 | 410 | context 'with config `Signature: sorbet`' do 411 | let(:cop_config) { { 'Signature' => 'sorbet' } } 412 | 413 | it 'autocorrects methods with Sorbet signatures' do 414 | new_source = autocorrect_source_file(<<~RUBY) 415 | class Foo 416 | # Comment b 417 | def b; end 418 | # Comment a 419 | sig { params(x: Integer).returns(String) } 420 | def a(x) 421 | x.to_s 422 | end 423 | alias_method :a2, :a 424 | end 425 | RUBY 426 | 427 | expect(new_source).to eq(<<~RUBY) 428 | class Foo 429 | # Comment a 430 | sig { params(x: Integer).returns(String) } 431 | def a(x) 432 | x.to_s 433 | end 434 | alias_method :a2, :a 435 | # Comment b 436 | def b; end 437 | end 438 | RUBY 439 | end 440 | 441 | it 'autocorrects two methods with the same signature' do 442 | new_source = autocorrect_source_file(<<~RUBY) 443 | class Foo 444 | # Comment b 445 | sig { params(id: ::String).returns(::Array) } 446 | def self.b(id) 447 | end 448 | 449 | sig { params(id: ::String).returns(::Array) } 450 | def self.a(id) 451 | end 452 | end 453 | RUBY 454 | 455 | expect(new_source).to eq(<<~RUBY) 456 | class Foo 457 | sig { params(id: ::String).returns(::Array) } 458 | def self.a(id) 459 | end 460 | 461 | # Comment b 462 | sig { params(id: ::String).returns(::Array) } 463 | def self.b(id) 464 | end 465 | end 466 | RUBY 467 | end 468 | end 469 | 470 | context 'with config `Signature: nil`' do 471 | let(:cop_config) { { 'Signature' => nil } } 472 | 473 | it 'ignores Sorbet signatures' do 474 | new_source = autocorrect_source_file(<<~RUBY) 475 | class Foo 476 | # Comment b 477 | def b; end 478 | # Comment a 479 | sig { params(x: Integer).returns(String) } 480 | def a(x) 481 | x.to_s 482 | end 483 | alias_method :a2, :a 484 | end 485 | RUBY 486 | 487 | expect(new_source).to eq(<<~RUBY) 488 | class Foo 489 | def a(x) 490 | x.to_s 491 | end 492 | alias_method :a2, :a 493 | # Comment a 494 | sig { params(x: Integer).returns(String) } 495 | # Comment b 496 | def b; end 497 | end 498 | RUBY 499 | end 500 | end 501 | 502 | context 'with configured method qualiifers' do 503 | let(:cop_config) { { 'MethodQualifiers' => %w[memoize] } } 504 | 505 | it 'recognizes the qualifier as a class method as well' do 506 | expect_offense(<<~RUBY) 507 | class Foo 508 | def b; end 509 | def a; end 510 | ^^^^^^^^^^ Methods should be sorted in alphabetical order. 511 | memoize :a 512 | end 513 | RUBY 514 | 515 | expect_correction(<<~RUBY) 516 | class Foo 517 | def a; end 518 | memoize :a 519 | def b; end 520 | end 521 | RUBY 522 | end 523 | 524 | it 'registers an offense when methods are not in alphabetical order' do 525 | expect_offense(<<~RUBY) 526 | class Foo 527 | def b; end 528 | memoize def a; end 529 | ^^^^^^^^^^^^^^^^^^ Methods should be sorted in alphabetical order. 530 | end 531 | RUBY 532 | 533 | expect_correction(<<~RUBY) 534 | class Foo 535 | memoize def a; end 536 | def b; end 537 | end 538 | RUBY 539 | end 540 | end 541 | 542 | # We integration-test our cop via `::RuboCop::CLI`. This is quite close to an 543 | # end-to-end test, with the normal pros and cons that entails. We exercise 544 | # more of our code, but our assertions are more fragile, for example asserting 545 | # very specific output. 546 | context 'when run via RuboCop CLI' do 547 | include_context 'mock console output' 548 | include FileHelper 549 | 550 | it 'does not register offense when methods are alphabetical' do 551 | cli = RuboCop::CLI.new 552 | file = Tempfile.new('rubocop_ordered_methods_spec_input.rb') 553 | create_file(file.path, <<~INPUT) 554 | class RTA 555 | def self.a; end 556 | def self.b; end 557 | end 558 | INPUT 559 | exit_status_code = 560 | cli.run([ 561 | '--require', 562 | 'rubocop-ordered_methods', 563 | '--format', 564 | 'simple', 565 | '--only', 566 | 'Layout/OrderedMethods', 567 | file.path 568 | ]) 569 | expect($stderr.string).to eq('') 570 | expect(exit_status_code).to eq(RuboCop::CLI::STATUS_SUCCESS) 571 | expect($stdout.string.strip).to eq('1 file inspected, no offenses detected') 572 | end 573 | 574 | it 'registers an offense when methods are not in alphabetical order' do 575 | cli = RuboCop::CLI.new 576 | file = Tempfile.new('rubocop_ordered_methods_spec_input.rb') 577 | create_file(file.path, <<~INPUT) 578 | class RTA 579 | def self.b; end 580 | def self.a; end 581 | end 582 | INPUT 583 | exit_status_code = 584 | cli.run([ 585 | '--require', 586 | 'rubocop-ordered_methods', 587 | '--format', 588 | 'simple', 589 | '--only', 590 | 'Layout/OrderedMethods', 591 | file.path 592 | ]) 593 | expect($stderr.string).to eq('') 594 | expect(exit_status_code).to eq(RuboCop::CLI::STATUS_OFFENSES) 595 | if RuboCop::Version::STRING >= '1.30.0' 596 | expect($stdout.string).to eq(<<~OUTPUT) 597 | == #{file.path} == 598 | C: 3: 3: [Correctable] Layout/OrderedMethods: Methods should be sorted in alphabetical order. 599 | 600 | 1 file inspected, 1 offense detected, 1 offense autocorrectable 601 | OUTPUT 602 | else 603 | expect($stdout.string).to eq(<<~OUTPUT) 604 | == #{file.path} == 605 | C: 3: 3: [Correctable] Layout/OrderedMethods: Methods should be sorted in alphabetical order. 606 | 607 | 1 file inspected, 1 offense detected, 1 offense auto-correctable 608 | OUTPUT 609 | end 610 | end 611 | end 612 | end 613 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'byebug' 4 | require 'rubocop' 5 | require 'rubocop/rspec/support' 6 | require 'rubocop-ordered_methods' 7 | 8 | # Require supporting files (custom matchers and macros, etc) 9 | # in ./support and its subdirectories. 10 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } 11 | 12 | RSpec.configure do |config| 13 | config.define_derived_metadata(file_path: %r{/spec/rubocop/cop/}) do |meta| 14 | meta[:type] = :cop_spec 15 | end 16 | 17 | # Enable flags like --only-failures and --next-failure 18 | config.example_status_persistence_file_path = 'tmp/.rspec_status' 19 | 20 | # Disable RSpec exposing methods globally on `Module` and `main` 21 | config.disable_monkey_patching! 22 | 23 | config.order = :random 24 | 25 | # We should address configuration warnings when we upgrade 26 | config.raise_errors_for_deprecations! 27 | 28 | # RSpec gives helpful warnings when you are doing something wrong. 29 | # We should take their advice! 30 | config.raise_on_warning = true 31 | 32 | config.include(RuboCop::RSpec::ExpectOffense) 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/file_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | # Copied from `rubocop/spec/support/file_helper.rb` 6 | module FileHelper 7 | def create_empty_file(file_path) 8 | create_file(file_path, '') 9 | end 10 | 11 | def create_file(file_path, content) 12 | file_path = File.expand_path(file_path) 13 | 14 | dir_path = File.dirname(file_path) 15 | FileUtils.mkdir_p dir_path 16 | 17 | File.open(file_path, 'w') do |file| 18 | case content 19 | when String 20 | file.puts content 21 | when Array 22 | file.puts content.join("\n") 23 | end 24 | end 25 | 26 | file_path 27 | end 28 | end 29 | --------------------------------------------------------------------------------