degem 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99c4e8766d6d3f8e6f710e2e13c0e0007190617fff3f83d198b469f0aec2ffb9
4
- data.tar.gz: 4df2c08f44fc1eb1c694a85ae55d36421f4610bb717f3789eb8b5feb93c726e3
3
+ metadata.gz: 4ca5d0f8ec44263d688f664f508c95477f5740aba98449817d1aee6728516c6c
4
+ data.tar.gz: 7dd84507228188120a8ee904e648fedc0116c639f83ba6c57f445a2d5267c69a
5
5
  SHA512:
6
- metadata.gz: 26145f7a46f9b9a3152092ba4fa7065261630b66c5edf443d7e90d1eff1c88264814edefc3048d73c39b24cb7a7e9709114baf5959133de351b841967f17fba0
7
- data.tar.gz: 5f3d721a7bf24c8541f93d5a67672ca71f8bfa7550a11822af4e214d6e338d905b9a4f8a3324457593de276c88cbb18eaf10af910faa8470a9abca9096787eca
6
+ metadata.gz: bee7cac14068642532355b2b88e205b1ebadc35ee8e8f6f31fea257079295e0cae4733d0f59cba95ddea9bc0404bf62cfc641790a98d0ae203630633cfdbeb2b
7
+ data.tar.gz: de01fdf89638fcb8bbbbc267a731e55197442fdaeb26d3b80567811b9f79accfac9a78fe07056b7ea0b993afe025acbbb2a8a80948ee0b78cc51dd89cdd3ed4c
data/lib/degem/cli.rb CHANGED
@@ -9,33 +9,29 @@ module Degem
9
9
  end
10
10
 
11
11
  def initialize(stderr)
12
- @stderr = stderr
12
+ Degem.stderr = stderr
13
13
  end
14
14
 
15
15
  def call
16
16
  unless gemfile_exists?
17
- @stderr.puts "Gemfile not found in the current directory"
17
+ Degem.stderr.puts "Gemfile not found in the current directory"
18
18
  return 1
19
19
  end
20
20
 
21
21
  unused = find_unused.call
22
22
  decorated = decorate_rubygems.call(unused)
23
- Report.new(@stderr).call(decorated)
23
+ Report.new.call(decorated)
24
24
  0
25
25
  end
26
26
 
27
27
  private
28
28
 
29
29
  def find_unused
30
- FindUnused.new(
31
- gemfile_path: GEMFILE,
32
- gem_specification: Gem::Specification,
33
- grep: Grep.new(@stderr)
34
- )
30
+ FindUnused.new(gemfile_path: GEMFILE)
35
31
  end
36
32
 
37
33
  def decorate_rubygems
38
- DecorateRubygems.new(
34
+ DecorateUnusedGems.new(
39
35
  gem_specification: Gem::Specification,
40
36
  git_adapter: GitAdapter.new
41
37
  )
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Degem
4
- class DecorateRubygems
4
+ class DecorateUnusedGems
5
5
  def initialize(gem_specification:, git_adapter:)
6
6
  @gem_specification = gem_specification
7
7
  @git_adapter = git_adapter
@@ -11,7 +11,7 @@ module Degem
11
11
  rubygems.map do |rubygem|
12
12
  gemspec = @gem_specification.find_by_name(rubygem.name)
13
13
  git = @git_adapter.call(rubygem.name)
14
- Rubygem.new(rubygem, gemspec, git)
14
+ UnusedGem.new(rubygem, gemspec, git)
15
15
  end
16
16
  end
17
17
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Degem
4
4
  class FindUnused
5
- def initialize(gemfile_path:, gem_specification:, grep: Grep.new, bundle_paths: GitLsFiles.new)
5
+ def initialize(gemfile_path:, gem_specification: Gem::Specification, bundle_paths: GitLsFiles.new)
6
6
  @gemfile_path = gemfile_path
7
7
  @gem_specification = gem_specification
8
- @grep = grep
9
- @bundle_paths = bundle_paths.call(File.dirname(gemfile_path))
8
+ fallback = Dir.glob(File.join(File.dirname(gemfile_path), "**/*.rb"))
9
+ @bundle_paths = bundle_paths.call(fallback)
10
10
  end
11
11
 
12
12
  def call
@@ -22,160 +22,40 @@ module Degem
22
22
  def reject_railties(rubygems)
23
23
  rubygems
24
24
  .reject { _1.name == "rails" }
25
- .reject do |rubygem|
26
- gem_path = @gem_specification.find_by_name(rubygem.name).full_gem_path
27
- @grep.match?(/(Rails::Railtie|Rails::Engine)/, gem_path)
28
- end
25
+ .reject { _1.consts.grep(/Rails::Railtie|Rails::Engine/).any? }
29
26
  end
30
27
 
31
28
  def reject_used(rubygems)
32
- candidates = rubygems.map { Matcher.new(rubygem: _1, matchers: matchers) }
33
- @grep.inverse_many(candidates, @bundle_paths).map(&:rubygem)
34
- end
35
-
36
- def matchers
37
- [
38
- method(:based_on_top_module),
39
- method(:based_on_top_composite_module_dash),
40
- method(:based_on_top_composite_module_underscore),
41
- method(:based_on_top_call),
42
- method(:based_on_top_composite_call_dash),
43
- method(:based_on_top_composite_call_underscore),
44
- method(:based_on_require),
45
- method(:based_on_require_prefix_path),
46
- method(:based_on_require_path)
47
- ].compact
48
- end
49
-
50
- def gemfile
51
- @gemfile ||= ParseGemfile.new.call(gemfile_path)
52
- end
53
-
54
- def rails?
55
- @rails ||= gemfile.rails?
56
- end
57
-
58
- # gem foo -> Foo:: (but not XFoo:: or X::Foo)
59
- def based_on_top_module(rubygem, line)
60
- return false if rubygem.name.include?("-")
61
-
62
- regex = %r{
63
- (?<!\w::) # Do not match if :: before
64
- (?<!\w) # Do not match if \w before
65
- #{rubygem.name.capitalize}
66
- ::
67
- }x
68
- regex.match?(line)
69
- end
70
-
71
- # gem foo_bar -> FooBar (but not XFooBar or X::FooBar)
72
- def based_on_top_composite_module_underscore(rubygem, line)
73
- return false unless rubygem.name.include?("_")
74
-
75
- regex = %r{
76
- (?<!\w::) # Do not match if :: before
77
- (?<!\w) # Do not match if \w before
78
- #{rubygem.name.split("_").map(&:capitalize).join}
79
- ::
80
- }x
81
- regex.match?(line)
29
+ bundle = ParseRuby.new.call(@bundle_paths)
30
+ rubygems = reject_required(rubygems, bundle.requires)
31
+ reject_consts(rubygems, bundle.consts)
82
32
  end
83
33
 
84
- # gem foo-bar -> Foo::Bar (but not XFoo::Bar or X::Foo::Bar)
85
- def based_on_top_composite_module_dash(rubygem, line)
86
- return false unless rubygem.name.include?("-")
87
-
88
- regex = %r{
89
- (?<!\w::) # Do not match if :: before
90
- (?<!\w) # Do not match if \w before
91
- #{rubygem.name.split("-").map(&:capitalize).join("::")}
92
- }x
93
- regex.match?(line)
94
- end
95
-
96
- # gem foo -> Foo. (but not X::Foo. or XBar.)
97
- def based_on_top_call(rubygem, line)
98
- return false if rubygem.name.include?("-")
99
-
100
- regex = %r{
101
- (?<!\w::) # Do not match if :: before
102
- (?<!\w) # Do not match if \w before
103
- #{rubygem.name.capitalize}
104
- \.
105
- }x
106
- regex.match?(line)
107
- end
108
-
109
- # gem foo-bar -> FooBar. (but not X::FooBar. or XFooBar.)
110
- def based_on_top_composite_call_dash(rubygem, line)
111
- return false unless rubygem.name.include?("-")
112
-
113
- regex = %r{
114
- (?<!\w::) # Do not match if :: before
115
- (?<!\w) # Do not match if \w before
116
- #{rubygem.name.split("-").map(&:capitalize).join}
117
- \.
118
- }x
119
- regex.match?(line)
34
+ def reject_consts(rubygems, bundle_consts)
35
+ rubygems.reject do |rubygem|
36
+ rubygem.own_consts.any? do |own_const|
37
+ bundle_consts.include?(own_const)
38
+ end
39
+ end
120
40
  end
121
41
 
122
- # gem foo_bar -> FooBar. (but not X::FooBar. or XFooBar.)
123
- def based_on_top_composite_call_underscore(rubygem, line)
124
- return false unless rubygem.name.include?("_")
42
+ def reject_required(rubygems, bundle_requires)
43
+ rubygems.reject do |rubygem|
44
+ bundle_requires.any? do |bundle_require|
45
+ next true if bundle_require == rubygem.name
46
+ next true if bundle_require == rubygem.name.tr("-", "/")
125
47
 
126
- regex = %r{
127
- (?<!\w::) # Do not match if :: before
128
- (?<!\w) # Do not match if \w before
129
- #{rubygem.name.split("_").map(&:capitalize).join}
130
- \.
131
- }x
132
- regex.match?(line)
133
- end
134
-
135
- # gem foo-bar -> require 'foo-bar'
136
- def based_on_require(rubygem, line)
137
- regex = %r{
138
- ^
139
- \s*
140
- require
141
- \s+
142
- ['"]
143
- #{rubygem.name}
144
- ['"]
145
- }x
146
- regex.match?(line)
48
+ bundle_require.start_with?("#{rubygem.name}/")
49
+ end
50
+ end
147
51
  end
148
52
 
149
- # gem foo-bar -> require 'foo/bar'
150
- def based_on_require_path(rubygem, line)
151
- return false unless rubygem.name.include?("-")
152
-
153
- regex = %r{
154
- ^
155
- \s*
156
- require
157
- \s+
158
- ['"]
159
- #{rubygem.name.tr("-", "/")} # match foo/bar when rubygem is foo-bar
160
- ['"]
161
- }x
162
- regex.match?(line)
53
+ def gemfile
54
+ @gemfile ||= ParseGemfile.new(@gem_specification).call(gemfile_path)
163
55
  end
164
56
 
165
- # gem foo -> require 'foo/'
166
- def based_on_require_prefix_path(rubygem, line)
167
- return false if rubygem.name.include?("-")
168
-
169
- regex = %r{
170
- ^
171
- \s*
172
- require
173
- \s+
174
- ['"]
175
- #{rubygem.name}
176
- /
177
- }x
178
- regex.match?(line)
57
+ def rails?
58
+ @rails ||= gemfile.rails?
179
59
  end
180
60
  end
181
61
  end
data/lib/degem/gemfile.rb CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  module Degem
4
4
  class Gemfile
5
- def initialize(dsl)
5
+ def initialize(dsl:, gem_specification:)
6
6
  @dsl = dsl
7
+ @gem_specification = gem_specification
7
8
  end
8
9
 
9
10
  def rubygems
10
- @rubygems ||= (gemfile_dependencies + gemspec_dependencies).uniq
11
+ @rubygems ||=
12
+ (gemfile_dependencies + gemspec_dependencies)
13
+ .map { Rubygem.new(rubygem: _1, gem_specification: @gem_specification) }
14
+ .uniq
11
15
  end
12
16
 
13
17
  def rails?
@@ -17,7 +21,9 @@ module Degem
17
21
  private
18
22
 
19
23
  def gemfile_dependencies
20
- @dsl.dependencies.select(&:should_include?)
24
+ @dsl.dependencies.select(&:should_include?).reject do |dependency|
25
+ @dsl.gemspecs.flat_map(&:name).include?(dependency.name)
26
+ end
21
27
  end
22
28
 
23
29
  def gemspec_dependencies
@@ -2,10 +2,14 @@
2
2
 
3
3
  module Degem
4
4
  class ParseGemfile
5
+ def initialize(gem_specification = Gem::Specification)
6
+ @gem_specification = gem_specification
7
+ end
8
+
5
9
  def call(gemfile_path)
6
10
  dsl = Bundler::Dsl.new
7
11
  dsl.eval_gemfile(gemfile_path)
8
- Gemfile.new(dsl)
12
+ Gemfile.new(dsl: dsl, gem_specification: @gem_specification)
9
13
  end
10
14
 
11
15
  private
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Degem
4
+ class ParseRuby
5
+ def initialize(visitor = Visitor)
6
+ @visitor = visitor
7
+ end
8
+
9
+ def call(path)
10
+ visitor = @visitor.new
11
+ Array(path).each do |path|
12
+ visitor.path = path
13
+ Prism.parse_file(path).value.accept(visitor)
14
+ Degem.stderr.putc "."
15
+ end
16
+ visitor
17
+ end
18
+ end
19
+
20
+ require "prism"
21
+
22
+ class Visitor < Prism::Visitor
23
+ def initialize
24
+ @requires = Set.new
25
+ @consts = Set.new
26
+ @path = nil
27
+ @stack = []
28
+ super
29
+ end
30
+
31
+ def requires = @requires.to_a
32
+ def consts = @consts.to_a
33
+ attr_writer :path
34
+
35
+ def visit_call_node(node)
36
+ visit_require_call_node(node)
37
+ super
38
+ end
39
+
40
+ def visit_module_node(node)
41
+ @stack.push(node)
42
+ super
43
+ @consts.add(@stack.map(&:name).join("::"))
44
+ @stack.pop
45
+ end
46
+
47
+ def visit_class_node(node)
48
+ @stack.push(node)
49
+ super
50
+ @consts.add(@stack.map(&:name).join("::"))
51
+ @stack.pop
52
+ end
53
+
54
+ def visit_constant_path_node(node)
55
+ consts_from(node).each { @consts.add(_1) }
56
+ super
57
+ end
58
+
59
+ def visit_constant_read_node(node)
60
+ @consts.add(node.name.to_s) unless @stack.find { _1.constant_path == node }
61
+ super
62
+ end
63
+
64
+ private
65
+
66
+ def visit_require_call_node(node)
67
+ return if node.name.to_s != "require"
68
+ return if node.receiver
69
+ return unless node.arguments
70
+ return unless node.arguments.arguments[0].is_a?(Prism::StringNode)
71
+
72
+ required = node.arguments.arguments[0].unescaped
73
+ @requires.add(required)
74
+ end
75
+
76
+ def from_ancestor_to(node)
77
+ acc = [node]
78
+ node = node.respond_to?(:parent) && node.parent
79
+ while node
80
+ acc.prepend(node)
81
+ node = node.respond_to?(:parent) && node.parent
82
+ end
83
+ acc
84
+ end
85
+
86
+ def consts_from(node)
87
+ from_ancestor_to(node)
88
+ .filter_map { _1.respond_to?(:name) ? _1.name.to_s : nil }
89
+ .tap { _1.singleton_class.include(Scan) }
90
+ .scan { |a, b| [a, b].join("::") }
91
+ end
92
+ end
93
+
94
+ module Scan
95
+ def scan(init = nil)
96
+ if init.nil?
97
+ init = self[0]
98
+ xs = self[1..] || []
99
+ else
100
+ xs = self
101
+ end
102
+
103
+ return self if xs.empty?
104
+
105
+ xs.reduce([init]) do |acc, x|
106
+ acc + [yield(acc.last, x)]
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/degem/report.rb CHANGED
@@ -2,21 +2,17 @@
2
2
 
3
3
  module Degem
4
4
  class Report
5
- def initialize(stderr)
6
- @stderr = stderr
7
- end
8
-
9
5
  def call(rubygems)
10
- @stderr.puts
11
- @stderr.puts
12
- @stderr.puts "The following gems may be unused:"
13
- @stderr.puts
6
+ Degem.stderr.puts
7
+ Degem.stderr.puts
8
+ Degem.stderr.puts "The following gems may be unused (#{rubygems.size}):"
9
+ Degem.stderr.puts
14
10
 
15
11
  rubygems.each do |rubygem|
16
12
  gem_name(rubygem)
17
- @stderr.puts
13
+ Degem.stderr.puts
18
14
  commits(rubygem)
19
- @stderr.puts
15
+ Degem.stderr.puts
20
16
  end
21
17
  end
22
18
 
@@ -30,15 +26,15 @@ module Degem
30
26
  "#{rubygem.name}: #{rubygem.source_code_uri}"
31
27
  end
32
28
 
33
- @stderr.puts(heading)
34
- @stderr.puts("=" * heading.size)
29
+ Degem.stderr.puts(heading)
30
+ Degem.stderr.puts("=" * heading.size)
35
31
  end
36
32
 
37
33
  def commits(rubygem)
38
34
  rubygem.commits.each.with_index do |commit, i|
39
- @stderr.puts("#{commit.hash[0..6]} (#{commit.date}) #{commit.title}")
40
- @stderr.puts(commit.url)
41
- @stderr.puts if i + 1 == rubygem.commits.size
35
+ Degem.stderr.puts("#{commit.hash[0..6]} (#{commit.date}) #{commit.title}")
36
+ Degem.stderr.puts(commit.url)
37
+ Degem.stderr.puts if i + 1 == rubygem.commits.size
42
38
  end
43
39
  end
44
40
  end
data/lib/degem/rubygem.rb CHANGED
@@ -1,16 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+
3
5
  module Degem
4
- class Rubygem < MultiDelegator
5
- attr_reader :commits
6
+ class Rubygem < SimpleDelegator
7
+ def initialize(rubygem:, gem_specification:)
8
+ @gem_specification = gem_specification
9
+ super(rubygem)
10
+ end
11
+
12
+ def consts
13
+ parsed.consts
14
+ end
15
+
16
+ def own_consts
17
+ variations = [
18
+ name,
19
+ name.delete("_-"),
20
+ name.gsub("_", "::"),
21
+ name.gsub("-", "::"),
22
+ *name.split("_").each_cons(2).to_a.map { _1.join("::") },
23
+ *name.split("_").each_cons(2).to_a.map(&:join),
24
+ *name.split("-").each_cons(2).to_a.map { _1.join("::") },
25
+ *name.split("-").each_cons(2).to_a.map(&:join)
26
+ ]
6
27
 
7
- def initialize(_, _, commits)
8
- super
9
- @commits = commits
28
+ consts.filter { |const| variations.any? { |variation| const.downcase == variation.downcase } }
10
29
  end
11
30
 
12
- def source_code_uri
13
- metadata["source_code_uri"] || homepage
31
+ private
32
+
33
+ def parsed
34
+ @parsed ||=
35
+ begin
36
+ gem_path = @gem_specification.find_by_name(name).full_gem_path
37
+ paths = Dir.glob(File.join(gem_path, "**/*.rb"))
38
+ ParseRuby.new.call(paths)
39
+ end
14
40
  end
15
41
  end
16
42
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Degem
4
- class Rubygem < MultiDelegator
4
+ class UnusedGem < MultiDelegator
5
5
  attr_reader :commits
6
6
 
7
7
  def initialize(_, _, commits)
data/lib/degem/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Degem
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/degem.rb CHANGED
@@ -2,18 +2,25 @@
2
2
 
3
3
  require_relative "degem/version"
4
4
  require_relative "degem/gemfile"
5
+ require_relative "degem/rubygem"
5
6
  require_relative "degem/parse_gemfile"
6
- require_relative "degem/grep"
7
7
  require_relative "degem/git_ls_files"
8
- require_relative "degem/matcher"
8
+ require_relative "degem/parse_ruby"
9
9
  require_relative "degem/find_unused"
10
10
  require_relative "degem/multi_delegator"
11
- require_relative "degem/rubygem"
12
- require_relative "degem/decorate_rubygems"
11
+ require_relative "degem/unused_gem"
12
+ require_relative "degem/decorate_unused_gems"
13
13
  require_relative "degem/commit"
14
14
  require_relative "degem/git_adapter"
15
15
  require_relative "degem/report"
16
16
  require_relative "degem/cli"
17
17
 
18
18
  module Degem
19
+ class << self
20
+ attr_writer :stderr
21
+
22
+ def stderr
23
+ @stderr ||= $stderr
24
+ end
25
+ end
19
26
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: degem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 3v0k4
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-05 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prism
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
13
27
  description: Degem finds unused gems in the Ruby bundle (ie, an app with a `Gemfile`
14
28
  or a gem with both a `Gemfile` and a gemspec).
15
29
  email:
@@ -23,18 +37,17 @@ files:
23
37
  - lib/degem.rb
24
38
  - lib/degem/cli.rb
25
39
  - lib/degem/commit.rb
26
- - lib/degem/decorate_rubygems.rb
27
- - lib/degem/decorated.rb
40
+ - lib/degem/decorate_unused_gems.rb
28
41
  - lib/degem/find_unused.rb
29
42
  - lib/degem/gemfile.rb
30
43
  - lib/degem/git_adapter.rb
31
44
  - lib/degem/git_ls_files.rb
32
- - lib/degem/grep.rb
33
- - lib/degem/matcher.rb
34
45
  - lib/degem/multi_delegator.rb
35
46
  - lib/degem/parse_gemfile.rb
47
+ - lib/degem/parse_ruby.rb
36
48
  - lib/degem/report.rb
37
49
  - lib/degem/rubygem.rb
50
+ - lib/degem/unused_gem.rb
38
51
  - lib/degem/version.rb
39
52
  homepage: https://github.com/3v0k4/degem
40
53
  licenses:
data/lib/degem/grep.rb DELETED
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "find"
4
-
5
- module Degem
6
- class Grep
7
- def initialize(stderr = StringIO.new)
8
- @stderr = stderr
9
- end
10
-
11
- def match?(matcher, dir)
12
- Find.find(File.expand_path(dir)) do |path|
13
- next unless File.file?(path)
14
- next if File.extname(path) != ".rb"
15
-
16
- @stderr.putc "."
17
- File.foreach(path) do |line|
18
- next unless matcher.match?(line)
19
-
20
- return true
21
- end
22
- end
23
-
24
- false
25
- end
26
-
27
- def inverse_many(matchers, paths)
28
- Find.find(*paths) do |path|
29
- next unless File.file?(path)
30
-
31
- @stderr.putc "."
32
- File.foreach(path) do |line|
33
- matchers = matchers.reject do |matcher|
34
- matcher.match?(line)
35
- end
36
- end
37
- end
38
-
39
- matchers
40
- end
41
- end
42
- end
data/lib/degem/matcher.rb DELETED
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Degem
4
- class Matcher
5
- attr_reader :rubygem
6
-
7
- def initialize(rubygem:, matchers:)
8
- @rubygem = rubygem
9
- @matchers = matchers
10
- end
11
-
12
- def match?(string)
13
- @matchers.any? { _1.call(@rubygem, string) }
14
- end
15
- end
16
- end