Skip to content

Commit

Permalink
feat: use prism to reject used
Browse files Browse the repository at this point in the history
  • Loading branch information
3v0k4 committed Dec 15, 2024
1 parent 649e26d commit dd9ff94
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 385 deletions.
7 changes: 3 additions & 4 deletions lib/degem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

require_relative "degem/version"
require_relative "degem/gemfile"
require_relative "degem/rubygem"
require_relative "degem/parse_gemfile"
require_relative "degem/grep"
require_relative "degem/git_ls_files"
require_relative "degem/matcher"
require_relative "degem/parse_ruby"
require_relative "degem/find_unused"
require_relative "degem/multi_delegator"
require_relative "degem/rubygem"
require_relative "degem/decorate_rubygems"
require_relative "degem/unused_gem"
require_relative "degem/decorate_unused_gems"
require_relative "degem/commit"
require_relative "degem/git_adapter"
require_relative "degem/report"
Expand Down
8 changes: 2 additions & 6 deletions lib/degem/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,11 @@ def call
private

def find_unused
FindUnused.new(
gemfile_path: GEMFILE,
gem_specification: Gem::Specification,
grep: Grep.new(@stderr)
)
FindUnused.new(gemfile_path: GEMFILE)
end

def decorate_rubygems
DecorateRubygems.new(
DecorateUnusedGems.new(
gem_specification: Gem::Specification,
git_adapter: GitAdapter.new
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Degem
class DecorateRubygems
class DecorateUnusedGems
def initialize(gem_specification:, git_adapter:)
@gem_specification = gem_specification
@git_adapter = git_adapter
Expand All @@ -11,7 +11,7 @@ def call(rubygems)
rubygems.map do |rubygem|
gemspec = @gem_specification.find_by_name(rubygem.name)
git = @git_adapter.call(rubygem.name)
Rubygem.new(rubygem, gemspec, git)
UnusedGem.new(rubygem, gemspec, git)
end
end
end
Expand Down
113 changes: 13 additions & 100 deletions lib/degem/find_unused.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

module Degem
class FindUnused
def initialize(gemfile_path:, gem_specification:, grep: Grep.new, bundle_paths: GitLsFiles.new)
def initialize(gemfile_path:, gem_specification: Gem::Specification, bundle_paths: GitLsFiles.new)
@gemfile_path = gemfile_path
@gem_specification = gem_specification
@grep = grep
fallback = Dir.glob(File.join(File.dirname(gemfile_path), "**/*.rb"))
@bundle_paths = bundle_paths.call(fallback)
end

def call
rubygems = gemfile.rubygems.reject { _1.name == "degem" }
rubygems = gemfile.rubygems.reject { ["degem"].include?(_1.name) }
rubygems = reject_railties(rubygems) if rails?
reject_used(rubygems)
end
Expand All @@ -23,19 +22,21 @@ def call
def reject_railties(rubygems)
rubygems
.reject { _1.name == "rails" }
.reject do |rubygem|
gem_path = @gem_specification.find_by_name(rubygem.name).full_gem_path
paths = Dir.glob(File.join(gem_path, "**/*.rb"))
parsed = ParseRuby.new.call(paths)
(parsed.consts.grep(/Rails::Railtie/) + parsed.consts.grep(/Rails::Engine/)).any?
end
.reject { _1.consts.grep(/Rails::Railtie|Rails::Engine/).any? }
end

def reject_used(rubygems)
bundle = ParseRuby.new.call(@bundle_paths)
rubygems = reject_required(rubygems, bundle.requires)
candidates = rubygems.map { Matcher.new(rubygem: _1, matchers: matchers) }
@grep.inverse_many(candidates, @bundle_paths).map(&:rubygem)
reject_consts(rubygems, bundle.consts)
end

def reject_consts(rubygems, bundle_consts)
rubygems.reject do |rubygem|
bundle_consts.any? do |bundle_const|
rubygem.own_consts.include?(bundle_const)
end
end
end

def reject_required(rubygems, requires)
Expand All @@ -49,100 +50,12 @@ def reject_required(rubygems, requires)
end
end

def matchers
[
method(:based_on_top_module),
method(:based_on_top_composite_module_dash),
method(:based_on_top_composite_module_underscore),
method(:based_on_top_call),
method(:based_on_top_composite_call_dash),
method(:based_on_top_composite_call_underscore)
].compact
end

def gemfile
@gemfile ||= ParseGemfile.new.call(gemfile_path)
@gemfile ||= ParseGemfile.new(@gem_specification).call(gemfile_path)
end

def rails?
@rails ||= gemfile.rails?
end

# gem foo -> Foo:: (but not XFoo:: or X::Foo)
def based_on_top_module(rubygem, line)
return false if rubygem.name.include?("-")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.capitalize}
::
}x
regex.match?(line)
end

# gem foo_bar -> FooBar (but not XFooBar or X::FooBar)
def based_on_top_composite_module_underscore(rubygem, line)
return false unless rubygem.name.include?("_")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.split("_").map(&:capitalize).join}
::
}x
regex.match?(line)
end

# gem foo-bar -> Foo::Bar (but not XFoo::Bar or X::Foo::Bar)
def based_on_top_composite_module_dash(rubygem, line)
return false unless rubygem.name.include?("-")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.split("-").map(&:capitalize).join("::")}
}x
regex.match?(line)
end

# gem foo -> Foo. (but not X::Foo. or XBar.)
def based_on_top_call(rubygem, line)
return false if rubygem.name.include?("-")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.capitalize}
\.
}x
regex.match?(line)
end

# gem foo-bar -> FooBar. (but not X::FooBar. or XFooBar.)
def based_on_top_composite_call_dash(rubygem, line)
return false unless rubygem.name.include?("-")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.split("-").map(&:capitalize).join}
\.
}x
regex.match?(line)
end

# gem foo_bar -> FooBar. (but not X::FooBar. or XFooBar.)
def based_on_top_composite_call_underscore(rubygem, line)
return false unless rubygem.name.include?("_")

regex = %r{
(?<!\w::) # Do not match if :: before
(?<!\w) # Do not match if \w before
#{rubygem.name.split("_").map(&:capitalize).join}
\.
}x
regex.match?(line)
end
end
end
15 changes: 12 additions & 3 deletions lib/degem/gemfile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

module Degem
class Gemfile
def initialize(dsl)
def initialize(dsl, gem_specification)
@dsl = dsl
@gem_specification = gem_specification
end

def rubygems
@rubygems ||= (gemfile_dependencies + gemspec_dependencies).uniq
@rubygems ||=
decorate(gemfile_dependencies + gemspec_dependencies)
.uniq
end

def rails?
Expand All @@ -17,11 +20,17 @@ def rails?
private

def gemfile_dependencies
@dsl.dependencies.select(&:should_include?)
@dsl.dependencies.select(&:should_include?).reject do |dependency|
@dsl.gemspecs.flat_map(&:name).include?(dependency.name)
end
end

def gemspec_dependencies
@dsl.gemspecs.flat_map(&:dependencies)
end

def decorate(rubygems)
rubygems.map { Rubygem.new(_1, @gem_specification) }
end
end
end
26 changes: 0 additions & 26 deletions lib/degem/grep.rb

This file was deleted.

16 changes: 0 additions & 16 deletions lib/degem/matcher.rb

This file was deleted.

6 changes: 5 additions & 1 deletion lib/degem/parse_gemfile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

module Degem
class ParseGemfile
def initialize(gem_specification = Gem::Specification)
@gem_specification = gem_specification
end

def call(gemfile_path)
dsl = Bundler::Dsl.new
dsl.eval_gemfile(gemfile_path)
Gemfile.new(dsl)
Gemfile.new(dsl, @gem_specification)
end

private
Expand Down
7 changes: 5 additions & 2 deletions lib/degem/parse_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,24 @@ def consts = @consts.to_a

def visit_require_call_node(node)
return if node.name.to_s != "require"
return if node.receiver
return unless node.arguments.arguments[0].is_a?(Prism::StringNode)

required = node.arguments.arguments[0].unescaped
@requires.add(required)
end

def collect_consts(node)
collect_consts_r(node)
.reject { |const| @consts.grep(/::#{const}\b/).any? }
.reject { |const| @consts.grep(/::#{Regexp.escape(const)}\b/).any? }
.scan { |a, b| [a, b].join("::") }
.each { @consts.add(_1) }
end

def collect_consts_r(node, acc = [])
return acc if node.nil?

acc += [node.respond_to?(:name) ? node.name.to_s : nil]
acc += [node.respond_to?(:name) ? node.name.to_s : ""]
node = children(node)[1]&.compact_child_nodes&.[](0)
collect_consts_r(node, acc)
end
Expand All @@ -95,6 +97,7 @@ def scan(init = nil)
end

return self if xs.empty?

xs.reduce([init]) do |acc, x|
acc + [yield(acc.last, x)]
end
Expand Down
40 changes: 33 additions & 7 deletions lib/degem/rubygem.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
# frozen_string_literal: true

require "delegate"

module Degem
class Rubygem < MultiDelegator
attr_reader :commits
class Rubygem < SimpleDelegator
def initialize(rubygem, gem_specification)
@gem_specification = gem_specification
super(rubygem)
end

def consts
parsed.consts
end

def own_consts
variations = [
name,
name.delete("_-"),
name.gsub("_", "::"),
name.gsub("-", "::"),
*name.split("_").each_cons(2).to_a.map { _1.join("::") },
*name.split("_").each_cons(2).to_a.map(&:join),
*name.split("-").each_cons(2).to_a.map { _1.join("::") },
*name.split("-").each_cons(2).to_a.map(&:join)
]

def initialize(_, _, commits)
super
@commits = commits
consts.filter { |const| variations.any? { |variation| const.downcase == variation.downcase } }
end

def source_code_uri
metadata["source_code_uri"] || homepage
private

def parsed
@parsed ||=
begin
gem_path = @gem_specification.find_by_name(name).full_gem_path
paths = Dir.glob(File.join(gem_path, "**/*.rb"))
ParseRuby.new.call(paths)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/degem/decorated.rb → lib/degem/unused_gem.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Degem
class Rubygem < MultiDelegator
class UnusedGem < MultiDelegator
attr_reader :commits

def initialize(_, _, commits)
Expand Down
Loading

0 comments on commit dd9ff94

Please sign in to comment.