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.
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?
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.
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
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'
When executed with rubocop target.rb
...
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'.
!!
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
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
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 ...
Oh, it looks good: thumbsup:
Do the same when substituting symbols and constants that look a lot like a string.
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 with
define_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
When executed as rubocop --only CustomCops / SpellInconsistency target.rb
...
Looks good: +1:
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.
target.rb
a = 'Fanclub'
b = :fanclub
c = FANCLUB
fanclub = 'a'
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.
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
...
fanclub ='a'
has been detected, but ... ʻa ='FanClub` has also been detected ...
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
When executed as rubocop --only CustomCops / SpellInconsistency target.rb
...
Looks good: tada:
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.
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
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
When executed with rspec spec / lib / custom_cops / spell_inconstency_spec.rb
...
Looks good: thumbsup:
I've defined variables, but I haven't defined constants yet, so I'll do it.
Short code for constant and method definitions.
target.rb
FANCLUB = 'a'
ruby-parse
Parse with ruby-parse
.
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
When executed as rubocop --only CustomCops / SpellInconsistency target.rb
...
Looks good: thumbsup:
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
At this point, I somehow understood the procedure.
Repeat this to improve the detection accuracy.
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.
Considering a short piece of code, it looks like def set_fanclub; hoge; end
.
Looking at ruby-parse ...
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
The argument is ʻarg, which has the same form as
lvasgn`, so we will summarize it.
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
You can give default values to the arguments, but since they were the same as str
, sym
, and const
, respectively, add nothing.
The keyword argument is kwarg
, which has the same form as lvasgn
, so we will summarize it.
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
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.
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
I remembered doing the keyword argument of the method definition. It was supported by the same symbol.
The class name and module name were const
.
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.
Recommended Posts