Writing Lint for Ruby
Masataka Kuwabara / Actcat, Inc.
RubyKaigi 2017
Sep. 20, 2017
© 2017 Actcat, Inc.
Masataka Kuwabara @pocke
Actcat, Inc. / SideCI
RuboCop’s core developer
Reek Collaborator
© 2017 Actcat, Inc.
Actcat, Inc / SideCI
SideCI reviews your Pull-Request automatically.
© 2017 Actcat, Inc.
How does SideCI review code?
© 2017 Actcat, Inc.
Goal
We can prevent known bugs by writing Lint.
© 2017 Actcat, Inc.
Agenda
© 2017 Actcat, Inc.
What’s Lint?
© 2017 Actcat, Inc.
What’s Lint?
© 2017 Actcat, Inc.
Example of Ruby(RuboCop)
# Use the && operator to
# compare multiple values.
if 10 < x < 20� do_something�end
Syntax is valid, but it’s an incorrect usage of `<` operator.
© 2017 Actcat, Inc.
Example of RuboCop
# foo(bar) { body }
# foo(bar { body })�foo bar { body }
�# x * y
# x(*y)�x *y
© 2017 Actcat, Inc.
Lint is static bug detector
© 2017 Actcat, Inc.
How does Lint work?
© 2017 Actcat, Inc.
Abstract Syntax Tree(AST)
if 1� p 'Hello'�end
s(:if,� s(:int, 1),� s(:send, nil, :p,� s(:str, "Hello")),
nil)
Ruby Code
AST(parser gem)
© 2017 Actcat, Inc.
Parser Gem
© 2017 Actcat, Inc.
Example of Parser gem
require 'parser/ruby24'�node = Parser::Ruby24.parse('if 1; p "hello"; end')
# => s(:if, s(:int, 1), ...)�node.type
# => :if�node.children
# => [s(:int, 1), s(:send, nil, :p, ...), nil]
© 2017 Actcat, Inc.
Metadata of Parser gem
node.loc.line # => 1�node.loc.column # => 0
node.loc.expression.source
# => 'if 1; p "Hello"; end'
node.loc.end
# => #<P::S::Range 17...20>
© 2017 Actcat, Inc.
Other parsers for Ruby
© 2017 Actcat, Inc.
Traverser
def traverse(node, visitor)� visitor.__send__(:"on_#{node.type}", node)� node.children.each do |child|� traverse(child, visitor) if child.is_a?(Parser::AST::Node)� end�end
© 2017 Actcat, Inc.
Traverser example
s(:if,� s(:int, 1),� s(:send,
nil, :p, s(:str, "Hello")),
nil)
© 2017 Actcat, Inc.
Visitor Pattern
class IntInCondVisitor� def on_if(node)� cond = node.children.first� if cond.type == :int� warn "Do not use an int literal in condition!!!" \� " (#{cond.loc.line}:#{cond.loc.column})"� end� end�� # TODO� def method_missing(*); end�end
© 2017 Actcat, Inc.
© 2017 Actcat, Inc.
What’s can / cannot Lint for Ruby do?
© 2017 Actcat, Inc.
A local variable is just a variable
# RuboCop warns about the code�if 1�end��# RuboCop does not warns the code.�num = 1�if num�end
© 2017 Actcat, Inc.
A local variable is just a variable
© 2017 Actcat, Inc.
Lint cannot know method / class / constant definition accurately
© 2017 Actcat, Inc.
Example: invalid sprintf() usage
# RuboCop says
# “number of arguments does not match”�sprintf('%s, %s', str)
# but maybe the `sprintf` is redefined.�def sprintf(t, s)� puts t + s�end
© 2017 Actcat, Inc.
Lint does not execute your code
© 2017 Actcat, Inc.
Lint can...
© 2017 Actcat, Inc.
How can I write Lint?
© 2017 Actcat, Inc.
Add a cop(rule) to RuboCop
© 2017 Actcat, Inc.
How to write a cop
© 2017 Actcat, Inc.
RuboCop Plugin
© 2017 Actcat, Inc.
New Lint Tool
© 2017 Actcat, Inc.
Flowchart to chose how to implement Lint
© 2017 Actcat, Inc.
Conclusion
© 2017 Actcat, Inc.