Static analysis gives you power to inspect project in seconds without reading any code – you can find heavy classes (flog), duplications (flay), check syntax (rubocop) and so on. In this post you will see code example of static #ruby analyser for searching one-letter variable names.

Analyser will be based on parser gem. So algorithm is quite simple – we need to detect if provided path is a path to file or a directory. Than recursively parse each file ( FileScanner class) and recursively find any variable assignment or calling (both instance and local, CodeAnalyzer class), check if variable name is long enough and output result with path to line of code in case of variable is one letter long ( NodeAnalyzer class):

require 'parser'
require 'parser/current'

module OneLetterVariableDetector
  class NodeAnalyzer < Struct.new(:node, :path)
    def analyze
      return unless node && node.respond_to?(:type)

      if node.type == :lvasgn || node.type == :lvar
        if lvar_name.size == 1
          puts "One letter local variable \"#{lvar_name}\" in #{path}:#{node.source_map.line}"
        end
      end

      if node.type == :ivasgn || node.type == :ivar
        if ivar_name.size == 1
          puts "One letter instance variable \"@#{ivar_name}\" in #{path}:#{node.source_map.line}"
        end
      end
    end

    private
    def lvar_name
      node.to_a.first.to_s.gsub(":", "")
    end

    def ivar_name
      node.to_a.first.to_s.gsub("@", "")
    end
  end

  class CodeAnalyzer < Struct.new(:path)
    def analyze
      Parser::CurrentRuby.parse(code).to_a.each do |lexem|
        scan_node(lexem)
      end
    end

    private
    def scan_node(node)
      node_analyzer = NodeAnalyzer.new(node, path)
      node_analyzer.analyze

      if node.respond_to?(:to_a) && node.to_a.respond_to?(:each)
        node.to_a.each do |subnode|
          scan_node(subnode)
        end
      end
    end

    def code
      @code ||= File.read(path)
    end
  end

  class FileScanner
    def scan(path)
      if File.directory?(path)
        scan_dir(path)
      else
        scan_file(path)
      end
    end

    private
    def scan_dir(path)
      Dir["#{path}/**/*.rb"].each do |file|
        scan_file(file)
      end
    end

    def scan_file(path)
      begin
        analyzer = CodeAnalyzer.new(path)
        analyzer.analyze
      rescue Exception => e
        # comment if you want to skip invalid files
        raise e
      end
    end
  end
end

scanner = OneLetterVariableDetector::FileScanner.new
scanner.scan("path_to_file_or_dir")

checkout gist page