1# frozen_string_literal: true
2require 'rubygems/command'
3require 'rubygems/local_remote_options'
4require 'rubygems/version_option'
5
6class Gem::Commands::DependencyCommand < Gem::Command
7
8  include Gem::LocalRemoteOptions
9  include Gem::VersionOption
10
11  def initialize
12    super 'dependency',
13          'Show the dependencies of an installed gem',
14          :version => Gem::Requirement.default, :domain => :local
15
16    add_version_option
17    add_platform_option
18    add_prerelease_option
19
20    add_option('-R', '--[no-]reverse-dependencies',
21               'Include reverse dependencies in the output') do
22      |value, options|
23      options[:reverse_dependencies] = value
24    end
25
26    add_option('-p', '--pipe',
27               "Pipe Format (name --version ver)") do |value, options|
28      options[:pipe_format] = value
29    end
30
31    add_local_remote_options
32  end
33
34  def arguments # :nodoc:
35    "REGEXP        show dependencies for gems whose names start with REGEXP"
36  end
37
38  def defaults_str # :nodoc:
39    "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies"
40  end
41
42  def description # :nodoc:
43    <<-EOF
44The dependency commands lists which other gems a given gem depends on.  For
45local gems only the reverse dependencies can be shown (which gems depend on
46the named gem).
47
48The dependency list can be displayed in a format suitable for piping for
49use with other commands.
50    EOF
51  end
52
53  def usage # :nodoc:
54    "#{program_name} REGEXP"
55  end
56
57  def fetch_remote_specs(dependency) # :nodoc:
58    fetcher = Gem::SpecFetcher.fetcher
59
60    ss, = fetcher.spec_for_dependency dependency
61
62    ss.map { |spec, _| spec }
63  end
64
65  def fetch_specs(name_pattern, dependency) # :nodoc:
66    specs = []
67
68    if local?
69      specs.concat Gem::Specification.stubs.find_all { |spec|
70        name_pattern =~ spec.name and
71          dependency.requirement.satisfied_by? spec.version
72      }.map(&:to_spec)
73    end
74
75    specs.concat fetch_remote_specs dependency if remote?
76
77    ensure_specs specs
78
79    specs.uniq.sort
80  end
81
82  def gem_dependency(pattern, version, prerelease) # :nodoc:
83    dependency = Gem::Deprecate.skip_during {
84      Gem::Dependency.new pattern, version
85    }
86
87    dependency.prerelease = prerelease
88
89    dependency
90  end
91
92  def display_pipe(specs) # :nodoc:
93    specs.each do |spec|
94      unless spec.dependencies.empty?
95        spec.dependencies.sort_by { |dep| dep.name }.each do |dep|
96          say "#{dep.name} --version '#{dep.requirement}'"
97        end
98      end
99    end
100  end
101
102  def display_readable(specs, reverse) # :nodoc:
103    response = String.new
104
105    specs.each do |spec|
106      response << print_dependencies(spec)
107      unless reverse[spec.full_name].empty?
108        response << "  Used by\n"
109        reverse[spec.full_name].each do |sp, dep|
110          response << "    #{sp} (#{dep})\n"
111        end
112      end
113      response << "\n"
114    end
115
116    say response
117  end
118
119  def execute
120    ensure_local_only_reverse_dependencies
121
122    pattern = name_pattern options[:args]
123
124    dependency =
125      gem_dependency pattern, options[:version], options[:prerelease]
126
127    specs = fetch_specs pattern, dependency
128
129    reverse = reverse_dependencies specs
130
131    if options[:pipe_format]
132      display_pipe specs
133    else
134      display_readable specs, reverse
135    end
136  end
137
138  def ensure_local_only_reverse_dependencies # :nodoc:
139    if options[:reverse_dependencies] and remote? and not local?
140      alert_error 'Only reverse dependencies for local gems are supported.'
141      terminate_interaction 1
142    end
143  end
144
145  def ensure_specs(specs) # :nodoc:
146    return unless specs.empty?
147
148    patterns = options[:args].join ','
149    say "No gems found matching #{patterns} (#{options[:version]})" if
150      Gem.configuration.verbose
151
152    terminate_interaction 1
153  end
154
155  def print_dependencies(spec, level = 0) # :nodoc:
156    response = String.new
157    response << '  ' * level + "Gem #{spec.full_name}\n"
158    unless spec.dependencies.empty?
159      spec.dependencies.sort_by { |dep| dep.name }.each do |dep|
160        response << '  ' * level + "  #{dep}\n"
161      end
162    end
163    response
164  end
165
166  def remote_specs(dependency) # :nodoc:
167    fetcher = Gem::SpecFetcher.fetcher
168
169    ss, _ = fetcher.spec_for_dependency dependency
170
171    ss.map { |s,o| s }
172  end
173
174  def reverse_dependencies(specs) # :nodoc:
175    reverse = Hash.new { |h, k| h[k] = [] }
176
177    return reverse unless options[:reverse_dependencies]
178
179    specs.each do |spec|
180      reverse[spec.full_name] = find_reverse_dependencies spec
181    end
182
183    reverse
184  end
185
186  ##
187  # Returns an Array of [specification, dep] that are satisfied by +spec+.
188
189  def find_reverse_dependencies(spec) # :nodoc:
190    result = []
191
192    Gem::Specification.each do |sp|
193      sp.dependencies.each do |dep|
194        dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep
195
196        if spec.name == dep.name and
197           dep.requirement.satisfied_by?(spec.version)
198          result << [sp.full_name, dep]
199        end
200      end
201    end
202
203    result
204  end
205
206  private
207
208  def name_pattern(args)
209    args << '' if args.empty?
210
211    if args.length == 1 and args.first =~ /\A\/(.*)\/(i)?\z/m
212      flags = $2 ? Regexp::IGNORECASE : nil
213      Regexp.new $1, flags
214    else
215      /\A#{Regexp.union(*args)}/
216    end
217  end
218end
219