1 of 35

Writing Lint for Ruby

Masataka Kuwabara / Actcat, Inc.

RubyKaigi 2017

Sep. 20, 2017

© 2017 Actcat, Inc.

2 of 35

Masataka Kuwabara @pocke

Actcat, Inc. / SideCI

RuboCop’s core developer

Reek Collaborator

© 2017 Actcat, Inc.

3 of 35

Actcat, Inc / SideCI

https://sideci.com

SideCI reviews your Pull-Request automatically.

  1. Open a Pull-Request on GitHub.
  2. SideCI reviews the Pull-Request.
  3. Fix reviewed code, or ignore the review.

© 2017 Actcat, Inc.

4 of 35

How does SideCI review code?

  • SideCI uses static analyzers such as Lint.
    • RuboCop
    • Reek
    • Brakeman
    • Querly

© 2017 Actcat, Inc.

5 of 35

Goal

  • You will be able to write:
    • Rules of Lint
    • Lint tools

We can prevent known bugs by writing Lint.

© 2017 Actcat, Inc.

6 of 35

Agenda

  • What’s Lint?
  • How does Lint work?
  • What’s can / cannot Lint do?
  • How can I write Lint?

© 2017 Actcat, Inc.

7 of 35

What’s Lint?

© 2017 Actcat, Inc.

8 of 35

What’s Lint?

  • Original Lint is a static analyzer for C proglam.
  • In this talk, Lint means a bug detector for any languages. For example:
    • JavaScript: ESLint
    • Python: Pylint
    • Ruby: RuboCop

© 2017 Actcat, Inc.

9 of 35

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.

10 of 35

Example of RuboCop

# foo(bar) { body }

# foo(bar { body })�foo bar { body }

# x * y

# x(*y)�x *y

© 2017 Actcat, Inc.

11 of 35

Lint is static bug detector

  • Lint detect code that can be a bug.
    • Invalid usage.
    • Ambiguous code.
      • They are valid about Syntax.

© 2017 Actcat, Inc.

12 of 35

How does Lint work?

© 2017 Actcat, Inc.

13 of 35

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.

14 of 35

Parser Gem

  • Parser gem parse ruby code to AST.
  • Many Lints use this gem.
    • RuboCop, Reek, Querly

© 2017 Actcat, Inc.

15 of 35

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.

16 of 35

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.

17 of 35

Other parsers for Ruby

  • Ripper
    • A standard library.
    • Runtime Ruby version == Parsed Ruby version.
  • ruby_parser
    • https://github.com/seattlerb/ruby_parser

© 2017 Actcat, Inc.

18 of 35

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)� endend

  • Depth-first search.
  • Call “on_#{node.type}”(e.g. on_send) each node.

© 2017 Actcat, Inc.

19 of 35

Traverser example

s(:if,� s(:int, 1),� s(:send,

nil, :p, s(:str, "Hello")),

nil)

© 2017 Actcat, Inc.

20 of 35

Visitor Pattern

class IntInCondVisitordef 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})"endend�� # TODOdef method_missing(*); endend

© 2017 Actcat, Inc.

21 of 35

© 2017 Actcat, Inc.

22 of 35

What’s can / cannot Lint for Ruby do?

© 2017 Actcat, Inc.

23 of 35

A local variable is just a variable

# RuboCop warns about the codeif 1end��# RuboCop does not warns the code.�num = 1if num�end

© 2017 Actcat, Inc.

24 of 35

A local variable is just a variable

  • Many Lint doesn’t trace local variables.
    • In `var = 1; puts var`.
      • “var” is just a variable, not an integer.
    • It is not impossible, but complexity.
      • For example: Brakeman can trace lvar.

© 2017 Actcat, Inc.

25 of 35

Lint cannot know method / class / constant definition accurately

  • In `foo(BAR, baz)`
    • Lint cannot know the foo method definition.
    • Lint cannot know the BAR class definition.

© 2017 Actcat, Inc.

26 of 35

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.

27 of 35

Lint does not execute your code

  • Lint doesn’t know:
    • Loaded library
    • Monkey patch
    • eval
    • User input value
  • Testing may be more appropriate in some cases.

© 2017 Actcat, Inc.

28 of 35

Lint can...

  • Understand AST.
    • Even if complex.
  • Analyze code without execution.
    • Faster than execution.

© 2017 Actcat, Inc.

29 of 35

How can I write Lint?

© 2017 Actcat, Inc.

30 of 35

Add a cop(rule) to RuboCop

  • For general cases.
    • `if 1 ; end`
    • Like `ruby -cw`
  • Easy to write.
    • RuboCop has many helper methods.
      • AST matcher, extended AST node.
    • RuboCop provides visitor, config file, etc.

© 2017 Actcat, Inc.

31 of 35

How to write a cop

  • Run `rake new_cop[Lint/NAME]` in RuboCop project.
    • DEMO

© 2017 Actcat, Inc.

32 of 35

RuboCop Plugin

  • For a specific framework / library.
    • For example: backus/rubocop-rspec
  • You can use RuboCop’s helpers.

© 2017 Actcat, Inc.

33 of 35

New Lint Tool

  • For:
    • Lint + X; e.g. Lint + Git Diff.
    • Ruby + X; e.g. Ruby + YAML.
  • It is out of scope of RuboCop.
    • You should create a new Lint tool.
    • Or use tool except RuboCop.

© 2017 Actcat, Inc.

34 of 35

Flowchart to chose how to implement Lint

  • If Ruby only:
    • if general cases:
      • Add a new rule to RuboCop.
    • else:
      • Create RuboCop plugin.
  • else:
    • Create a new Lint tool.

© 2017 Actcat, Inc.

35 of 35

Conclusion

  • Lint is static bug detector.
  • Lint traverses AST.
  • Lint does not execute your code.
  • There are several choices for writing Lint.

© 2017 Actcat, Inc.