Last active
December 11, 2019 01:15
-
-
Save iliabylich/06c0a67244553742efd06d069fc70cb6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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