Let's make a custom_cop that points out the shaking of the name

Introduction: cat2:

Fanclub and fan_club are mixed in the code ... I think it's common. I'd like to unify it to those who say that this is the one in the team, but I don't have enough memory in my head to remember such rules ... It's also a good idea to have people point out, and I'd like to leave it to a computer that patiently points out many times. So, I made a custom_cop for Rubocop.

Source here.

By the way, even if you don't use such troublesome work, grep to the diff with the master branch seems to be easier. But I wanted to write custom_cop. It ’s a Sunday program, right?

Simple specifications: cake:

If you try to define what you want as far as you can understand ... Is it like this? Keywords increase many times during application development, so I want to define them in an external file.

List the wrong words and the words you want to correct in the YAML file, point out any wrong words, and correct them.

Try making it: muscle:

Anyway, it's not interesting if it doesn't work, so I'll do my best to aim for: point_down :.

Point out if the value you put in the variable is the wrong word fanclub

Check target code: dart:

Prepare the code that can be checked when rubocop is run. I think the code of the application you are actually using is fine.

target.rb


a = 'fanclub'

custom_cop :cop:

I made it almost by copying. Anyway, I hope you can move like that.

lib/custom_cops/spell_inconsistency.rb


# frozen_string_literal: true

module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
    WRONG_KEYWORD = 'fanclub'.freeze

    def on_str(node)
      add_offense(node, message: "Use 'fan_club' instead of 'fanclub'.") if node.source.include?(WRONG_KEYWORD)
    end
  end
end

.rubocop.yml :wrench:

Set the created custom_cop so that it can be used by rubocop.

yaml:.rubocop.yml


require:
  - './lib/custom_cops/spell_inconsistency'

Run: raised_hands:

When executed with rubocop target.rb ...

image.png

In addition to pointing out I'm not using variables just for definition orBecause it's a constant, please freeze it... CustomCops / SpellInconsistency: Use'fan_club' instead of'fanclub'. !!

Other words are also supported: zap:

I would like to extend it to the one registered in YAML.

Point out if the value you put in the variable is the wrong word in the YAML file

spell_inconsistency.yml :wrench:

Here, register fanclub, Fanclub, and FANCLUB. In the future, I would like to write them in two words, fan_club, FanClub, and FAN_CLUB, respectively.

lib/custom_cops/spell_inconsistency.yml


# Wrong: Correct
fanclub: fan_club
Fanclub: Fanclub
FANCLUB: FAN_CLUB

Check target code: dart:

target.rb


a = 'fanclub'
b = 'Fanclub'
c = 'FANCLUB'

custom_cop :cop:

The file in which the word is registered is read by YAML.load_file, and it is checked by turning it by ʻeach`.

lib/custom_cops/spell_inconsistency.rb


# frozen_string_literal: true

require 'yaml'

module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
    MESSAGE_TEMPLATE = "Use '%s' instead of '%s'."
    SPELL_INCONSISTENCIES = YAML.load_file(Pathname(__dir__).join('spell_inconsistency.yml'))

    def on_str(node)
      SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
        add_offense(node, message: message(wrong_keyword, correct_keyword)) if node.source.include?(wrong_keyword)
      end
    end

    private

    def message(wrong_keyword, correct_keyword)
      MESSAGE_TEMPLATE % [correct_keyword, wrong_keyword]
    end
  end
end

Run: raised_hands:

If you execute it with rubocop target.rb, there are many extra messages, so if you specify only your own custom_cop like rubocop --only CustomCops / SpellInconsistency target.rb and execute it ...

image.png

Oh, it looks good: thumbsup:

Substitute symbols and constants

Do the same when substituting symbols and constants that look a lot like a string.

Check target code: dart:

target.rb


a = 'Fanclub'
b = :fanclub
c = FANCLUB

custom_cop :cop:

Before writing, take a look at RuboCop :: AST :: Traversal.

Immediately after seeing # walk Seeing the type of node that came in, it seems to call the method by that name. So it seems that my #on_str was also called.

lib/rubocop/ast/traversal.rb


def walk(node)
  return if node.nil?

  send(:"on_#{node.type}", node)
  nil
end

I searched all over the file and found const and sym like that, so I will implement # on_sym and ʻon_constas well. Since the inspection method etc. are exactly the same, define withdefine_method`.

lib/custom_cops/spell_inconsistency.rb


# frozen_string_literal: true

require 'yaml'

module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
    MESSAGE_TEMPLATE = "Use '%s' instead of '%s'."
    SPELL_INCONSISTENCIES = YAML.load_file(Pathname(__dir__).join('spell_inconsistency.yml'))

    NODE_TYPES = %I[str const sym].freeze
    NODE_TYPES.each do |node_type|
      define_method "on_#{node_type}" do |node|
        SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
          add_offense(node, message: message(wrong_keyword, correct_keyword)) if node.source.include?(wrong_keyword)
        end
      end
    end

    def message(wrong_keyword, correct_keyword)
      MESSAGE_TEMPLATE % [correct_keyword, wrong_keyword]
    end
  end
end

Run: raised_hands:

When executed as rubocop --only CustomCops / SpellInconsistency target.rb ...

image.png

Looks good: +1:

Point out if you use the wrong word in the variable name

Now that we have strings, symbols, and constants, let's get them to point out if we use the wrong word for the variable name.

Check target code: dart:

target.rb


a = 'Fanclub'
b = :fanclub
c = FANCLUB
fanclub = 'a'

ruby-parse part 1

Speaking of variables, variable would be var ... So, [RuboCop :: AST :: Traversal](https://github.com/rubocop-hq/rubocop-ast/blob/master/lib I took a look at (/rubocop/ast/traversal.rb), but ... there isn't much of it, and there are many ...

Looking at Development Basic of the official RuboCop documentation, it says to use ruby-parse on the command line. I understand.

image.png

It was lvasgn. Add to NODE_TYPES ...

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
    (abridgement)
    NODE_TYPES = %I[str const sym lvasgn].freeze
    NODE_TYPES.each do |node_type|
      define_method "on_#{node_type}" do |node|
        SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
          add_offense(node, message: message(wrong_keyword, correct_keyword)) if node.source.include?(wrong_keyword)
        end
      end
    end
    (abridgement)
  end
end

When you run rubocop --only CustomCops / SpellInconsistency target.rb ...

image.png

fanclub ='a' has been detected, but ... ʻa ='FanClub` has also been detected ...

ruby-parse (2)

Let's take a closer look at the original code and the output of ruby-parse.

(lvasgn :fanclub
  (str "a"))

The assignment to a variable is variable name = expression, isn't it? I wonder if the expression corresponds to the character string ʻa. If you look at it like this, lvasgn is the left value a sign` of left side assignment. (Since there was a parsing in a university class long ago, I can't explain it, but I can't explain it at a level that I can understand ... I'm sorry.)

(Left side substitution:fanclub
  (String"a")) 

The reason I was stuck twice with fanclub ='a' was that it responded to"fanclub"of str and to " fanclub " of str in lvasgn. Probably from.

(lvasgn :a
  (str "fanclub"))

It seems that you should take it only immediately after lvasgn.

If you review Development Basic of RuboCop official document, you can see the methods often used in the argument node of ʻon_ ~. Was there. You should use children` and use its first child.

node.type # => :send
node.children # => [s(:send, s(:send, nil, :something), :empty?), :!]
node.source # => "!something.empty?"

custom_cop :cop: Apart from str, const, sym, I made ʻon_lvasgn` to inspect the first child.

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    NODE_TYPES = %I[str const sym].freeze
    NODE_TYPES.each do |node_type|
      define_method "on_#{node_type}" do |node|
        SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
          add_offense(node, message: message(wrong_keyword, correct_keyword)) if node.source.include?(wrong_keyword)
        end
      end
    end

    def on_lvasgn(node)
      target = node.children.first
      SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
        add_offense(node, message: message(wrong_keyword, correct_keyword)) if target.match?(/#{wrong_keyword}/)
      end
    end
(abridgement)
  end
end

Run: raised_hands:

When executed as rubocop --only CustomCops / SpellInconsistency target.rb ...

image.png

Looks good: tada:

Test (RSpec): pencil:

What we've done so far is ... checking strings, symbols, constants and variable names. Considering the syntax of Ruby ... There are method names and class names just to come up with the parts that should be dealt with. You will notice something after this ... Then, it was troublesome to execute the inspection code one by one, so I wanted to write a test.

Settings: wrench:

The files under spec / support are required. It seems that people who are trying to enter the application later are often already set up.

spec/spec_helper.rb


RSpec.configure do |config|
(abridgement)
  Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
end

I'm loading RSpec support for rubocop and custom_cop I added myself.

spec/support/rubocop.rb


# frozen_string_literal: true

require 'rubocop'
require 'rubocop/rspec/support'
Dir["#{__dir__}/../../lib/**/*.rb"].sort.each { |f| require f }

RSpec.configure do |config|
  config.include(RuboCop::RSpec::ExpectOffense)
end

Test: pencil:

The one written in the check target code is transcribed to the test, and how the output when rubocop is executed is also described.

spec/lib/custom_cops/spell_inconstency_spec.rb


# frozen_string_literal: true

RSpec.describe CustomCops::SpellInconsistency do
  subject(:cop) { described_class.new }

  it 'Being able to detect mistakes in character strings' do
    expect_offense(<<-RUBY)
      fan_club = 'fanclub'
                 ^^^^^^^^^ Use 'fan_club' instead of 'fanclub'.
    RUBY
  end

  it 'Being able to detect mistakes in symbols' do
    expect_offense(<<-RUBY)
      fan_club = :fanclub
                 ^^^^^^^^ Use 'fan_club' instead of 'fanclub'.
    RUBY
  end

  it 'Being able to detect constant mistakes' do
    expect_offense(<<-RUBY)
      fan_club = FANCLUB
                 ^^^^^^^ Use 'FAN_CLUB' instead of 'FANCLUB'.
    RUBY
  end

  it 'Being able to detect mistakes in variable names' do
    expect_offense(<<-RUBY)
      fanclub = 'fan_club'
      ^^^^^^^^^^^^^^^^^^^^ Use 'fan_club' instead of 'fanclub'.
    RUBY
  end
end

Run: raised_hands:

When executed with rspec spec / lib / custom_cops / spell_inconstency_spec.rb ...

image.png

Looks good: thumbsup:

Constant definition

I've defined variables, but I haven't defined constants yet, so I'll do it.

Check target code: dart:

Short code for constant and method definitions.

target.rb


FANCLUB = 'a'

ruby-parse

Parse with ruby-parse.

image.png

The constant definition is casgn, which is a different format than lvasgn. It was the second.

custom_cop :cop: I made a check method for casgn.

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    def on_casgn(node)
      target = node.children[1]
      SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
        add_offense(node, message: message(wrong_keyword, correct_keyword)) if target.match?(/#{wrong_keyword}/)
      end
    end
(abridgement)
  end
end

Run: raised_hands:

When executed as rubocop --only CustomCops / SpellInconsistency target.rb ...

image.png

Looks good: thumbsup:

Test: pencil:

Add it to the test so that you will notice if it breaks.

spec/lib/custom_cops/spell_inconstency_spec.rb


# frozen_string_literal: true

RSpec.describe CustomCops::SpellInconsistency do
  subject(:cop) { described_class.new }
(abridgement)
  it 'Being able to detect mistakes in constant names' do
    expect_offense(<<-RUBY)
      FANCLUB = 'fan_club'
      ^^^^^^^^^^^^^^^^^^^^ Use 'FAN_CLUB' instead of 'FANCLUB'.
    RUBY
  end
end

Brush up: sparkles:

At this point, I somehow understood the procedure.

  1. Write the code piece you want to detect (= write the test)
  2. Analyze with ruby-parse
  3. Implement

Repeat this to improve the detection accuracy.

Method definition

We will give it a name when we define the method, so let's take a look. Since there are many elements in one line and there are many types of elements, we will cut them into small pieces.

Method name

Considering a short piece of code, it looks like def set_fanclub; hoge; end.

Looking at ruby-parse ...

image.png

The first child of def is like that. It's the same as the lvasgn used when assigning to a variable. Like str and const, use define_method to put them together in a loop.

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    NODE_TYPES_ONE = %I[str const sym].freeze
    NODE_TYPES_FIRST_CHILD = %I[lvasgn def].freeze

    NODE_TYPES_ONE.each do |node_type|
      define_method "on_#{node_type}" do |node|
        SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
          add_offense(node, message: message(wrong_keyword, correct_keyword)) if node.source.include?(wrong_keyword)
        end
      end
    end

    NODE_TYPES_FIRST_CHILD.each do |node_type|
      define_method "on_#{node_type}" do |node|
        target = node.children.first
        SPELL_INCONSISTENCIES.each do |wrong_keyword, correct_keyword|
          add_offense(node, message: message(wrong_keyword, correct_keyword)) if target.match?(/#{wrong_keyword}/)
        end
      end
    end
(abridgement)
  end
end

argument

The argument is ʻarg, which has the same form as lvasgn`, so we will summarize it.

image.png

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    NODE_TYPES_FIRST_CHILD = %I[lvasgn def arg].freeze
(abridgement)
  end
end

Default value of argument

You can give default values to the arguments, but since they were the same as str, sym, and const, respectively, add nothing.

image.png

Keyword arguments

The keyword argument is kwarg, which has the same form as lvasgn, so we will summarize it.

image.png

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    NODE_TYPES_FIRST_CHILD = %I[lvasgn def arg kwarg].freeze
(abridgement)
  end
end

Keyword option argument

When the default value is given to the keyword argument, the argument name is kwoptarg, which is the same form as lvasgn, so I will summarize it.

image.png

lib/custom_cops/spell_inconsistency.rb


(abridgement)
module CustomCops
  class SpellInconsistency < RuboCop::Cop::Cop
(abridgement)
    NODE_TYPES_FIRST_CHILD = %I[lvasgn def arg kwarg kwoptarg].freeze
(abridgement)
  end
end

hash

I remembered doing the keyword argument of the method definition. It was supported by the same symbol.

image.png

Class definition, module definition

The class name and module name were const.

image.png

image.png

Try it

I've come this far ... I feel like there are still a lot of implementation omissions. However, since I am writing a test, I feel that I can gradually grow it based on what I noticed.

But ... it was super easy to do, but it was insanely difficult. No way, I'm gonna bite the parsing ... However, I made friends with Rubocop. Also, if you want to make a rule that you can't remember with your work code, I'll try it.

reference

Recommended Posts

Let's make a custom_cop that points out the shaking of the name
Format Ruby with VS Code
Let's make a custom_cop that points out the shaking of the name
Whether the total product of the first n prime numbers plus 1 is a prime number
Find the remainder divided by 3 without using a number
Let's make a custom_cop that points out the shaking of the name
Let's write a code that is easy to maintain (Part 2) Name