Skip to content

Instantly share code, notes, and snippets.

@iliabylich
Last active December 11, 2019 01:15
Show Gist options
  • Save iliabylich/06c0a67244553742efd06d069fc70cb6 to your computer and use it in GitHub Desktop.
Save iliabylich/06c0a67244553742efd06d069fc70cb6 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'bundler/setup'
require 'parser/current'
require 'pry'
def each_file
return to_enum(:each_file) unless block_given?
Dir['app/**/*.rb'].sort.each { |f| yield f }
end
def parse_file(file)
Parser::CurrentRuby.parse(File.read(file), file)
end
CONST_SUFFIX = /::([\w\d]+)\z/
def each_namespace_of(const_name)
return to_enum(:each_namespace_of, const_name) unless block_given?
while const_name =~ CONST_SUFFIX
const_name = const_name.sub(CONST_SUFFIX, '')
yield const_name
end
end
class Processor < Parser::AST::Processor
def initialize(&emitter)
@nesting = []
@emitter = emitter
end
def each_nesting
return to_enum(:each_nesting) unless block_given?
current = @nesting.dup
yield current
while current.any?
current.pop
yield current
end
end
def emit(name:, event:, node:)
@emitter.call(name: name, event: event, node: node)
end
def emit_defined(const_name, node)
full_class_name = [*@nesting, const_name].join('::')
emit(name: full_class_name, event: :defined, node: node)
end
def emit_used(const_name, node)
each_nesting do |nesting|
full_class_name = [*nesting, const_name].join('::')
emit(name: full_class_name, event: :probably_used, node: node)
each_namespace_of(full_class_name) do |ns|
emit(name: ns, event: :probably_used, node: node)
end
end
end
def on_class(node)
defined_class, superclass, body = *node
defined_class_name = const_name(defined_class)
emit_defined(defined_class_name, defined_class)
if superclass
superclass_name = const_name(superclass)
emit_used(superclass_name, superclass)
end
@nesting.push(defined_class_name)
process(body)
@nesting.pop
end
def on_module(node)
defined_module, body = *node
defined_module_name = const_name(defined_module)
emit_defined(defined_module_name, defined_module)
@nesting.push(defined_module_name)
process(body)
@nesting.pop
end
def on_const(node)
const_name = self.const_name(node)
emit_used(const_name, node)
end
def on_casgn(node)
scope, name, value = *node
scope_name = const_name(scope)
const_name = [scope_name, name].compact.join('::')
emit_defined(const_name, node)
process(value)
end
def const_name(node)
case node
when AST::Node
return nil if node.type == :cbase # ::Current
scope, name = *node
[const_name(scope), name].compact.join('::')
when :sym
node
when nil
# end of the chain
else
raise "Unsupported node #{node}"
end
end
def on_send(node)
recv, mid, *args = *node
file = node.location.expression.source_buffer.name
if file =~ /\Aapp\/models\// && mid == :has_many
hash_arg = args.detect { |arg| arg.type == :hash }
if hash_arg
pairs = hash_arg.to_a.map(&:to_a)
class_name_pair = pairs.detect { |(k, v)| k.type == :sym && k.children[0] == :class_name }
if class_name_pair
_, class_name_node = *class_name_pair
class_name = class_name_node.children[0]
emit_used(class_name, class_name_node)
end
end
end
super
end
end
Const = Struct.new(:name, :file, :node)
def extract_defined_constants(file, ast)
defined = []
probably_used = []
visitor = Processor.new do |name:, node:, event:|
# p [event, name]
const = Const.new(name, file, node)
case event
when :defined then defined << const
when :probably_used then probably_used << const
else
raise "Unsupported event #{event}"
end
end
visitor.process(ast)
[probably_used, defined]
end
defined_constants = Set.new
probably_used_constants = Set.new
each_file do |file|
puts "Parsing #{file}..."
ast = parse_file(file)
probably_used, defined = extract_defined_constants(file, ast)
probably_used.each { |c| probably_used_constants << c }
defined.each { |c| defined_constants << c }
end
def ignore?(const, defined_constants)
return true if /(Controller|Serializer|Input|Helper|Validator)\z/ =~ const.name
false
end
defined_constants.each do |defined|
probably_used = probably_used_constants.any? { |used| used.name == defined.name }
next if probably_used
should_ignore = ignore?(defined, defined_constants)
next if should_ignore
puts "Unused #{defined.name} (defined in #{defined.file})"
end
binding.pry
binding.pry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment