ripple_effect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.ripple-effect.yml.example +56 -0
  3. data/ARCHITECTURE.md +222 -0
  4. data/CHANGELOG.md +115 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/CONTRIBUTING.md +112 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +305 -0
  9. data/SECURITY.md +73 -0
  10. data/docs/ANALYSIS_MODEL.md +275 -0
  11. data/docs/CLI.md +276 -0
  12. data/docs/CONFIGURATION.md +178 -0
  13. data/docs/DECISIONS.md +210 -0
  14. data/docs/PUBLIC_LAUNCH_CHECKLIST.md +105 -0
  15. data/docs/RELEASING.md +94 -0
  16. data/docs/TESTING.md +179 -0
  17. data/exe/ripple-effect +7 -0
  18. data/lib/ripple_effect/analyzer.rb +379 -0
  19. data/lib/ripple_effect/cache_store.rb +207 -0
  20. data/lib/ripple_effect/cli/application.rb +126 -0
  21. data/lib/ripple_effect/cli/command.rb +165 -0
  22. data/lib/ripple_effect/cli/diff_command.rb +76 -0
  23. data/lib/ripple_effect/cli/doctor_command.rb +106 -0
  24. data/lib/ripple_effect/cli/graph_command.rb +61 -0
  25. data/lib/ripple_effect/cli/inspect_command.rb +66 -0
  26. data/lib/ripple_effect/cli/tests_command.rb +109 -0
  27. data/lib/ripple_effect/cli/version_command.rb +46 -0
  28. data/lib/ripple_effect/confidence.rb +61 -0
  29. data/lib/ripple_effect/configuration.rb +264 -0
  30. data/lib/ripple_effect/diagnostic.rb +90 -0
  31. data/lib/ripple_effect/diff/changed_symbol_resolver.rb +292 -0
  32. data/lib/ripple_effect/diff/git.rb +175 -0
  33. data/lib/ripple_effect/diff/hunk.rb +80 -0
  34. data/lib/ripple_effect/edge.rb +114 -0
  35. data/lib/ripple_effect/error.rb +23 -0
  36. data/lib/ripple_effect/extractors/base.rb +292 -0
  37. data/lib/ripple_effect/extractors/rails_associations.rb +102 -0
  38. data/lib/ripple_effect/extractors/rails_callbacks.rb +144 -0
  39. data/lib/ripple_effect/extractors/rails_delegation.rb +121 -0
  40. data/lib/ripple_effect/extractors/rails_jobs.rb +131 -0
  41. data/lib/ripple_effect/extractors/rails_mailers.rb +120 -0
  42. data/lib/ripple_effect/extractors/rails_routes.rb +256 -0
  43. data/lib/ripple_effect/extractors/rails_views.rb +299 -0
  44. data/lib/ripple_effect/extractors/ruby_structure.rb +221 -0
  45. data/lib/ripple_effect/extractors/test_conventions.rb +135 -0
  46. data/lib/ripple_effect/formatters/dot.rb +69 -0
  47. data/lib/ripple_effect/formatters/json.rb +43 -0
  48. data/lib/ripple_effect/formatters/text.rb +197 -0
  49. data/lib/ripple_effect/graph.rb +199 -0
  50. data/lib/ripple_effect/node.rb +153 -0
  51. data/lib/ripple_effect/project.rb +264 -0
  52. data/lib/ripple_effect/result.rb +147 -0
  53. data/lib/ripple_effect/risk.rb +167 -0
  54. data/lib/ripple_effect/static_index/adapter.rb +84 -0
  55. data/lib/ripple_effect/static_index/rubydex_adapter.rb +356 -0
  56. data/lib/ripple_effect/traversal/impact_walker.rb +153 -0
  57. data/lib/ripple_effect/version.rb +11 -0
  58. data/lib/ripple_effect.rb +89 -0
  59. metadata +155 -0
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "command"
5
+ require_relative "inspect_command"
6
+ require_relative "diff_command"
7
+ require_relative "tests_command"
8
+ require_relative "graph_command"
9
+ require_relative "doctor_command"
10
+ require_relative "version_command"
11
+
12
+ module RippleEffect
13
+ module CLI
14
+ # Parses global flags, dispatches to a subcommand, and owns the process's
15
+ # exit code. All CLI parsing lives here and in {Command}; no domain object
16
+ # ever sees an ARGV.
17
+ class Application
18
+ COMMANDS = {
19
+ "inspect" => InspectCommand,
20
+ "diff" => DiffCommand,
21
+ "tests" => TestsCommand,
22
+ "graph" => GraphCommand,
23
+ "doctor" => DoctorCommand,
24
+ "version" => VersionCommand
25
+ }.freeze
26
+
27
+ BANNER = <<~BANNER
28
+ Usage: ripple-effect COMMAND [options]
29
+
30
+ Change-impact analysis for Ruby on Rails.
31
+ Ripple Effect is deterministic and local: it never boots your application,
32
+ connects to your database, or makes a network call.
33
+
34
+ Commands:
35
+ inspect SYMBOL What may be affected by changing SYMBOL, and why
36
+ diff BASE [HEAD] Blast radius of everything changed between revisions
37
+ tests BASE [HEAD] Test files most likely relevant to a change
38
+ graph SYMBOL Emit the impacted subgraph as DOT or JSON
39
+ doctor Check this project and report what Ripple Effect sees
40
+ version Print the version
41
+
42
+ Examples:
43
+ ripple-effect inspect 'BillingService#charge'
44
+ ripple-effect diff main
45
+ bundle exec rspec $(ripple-effect tests main)
46
+ ripple-effect graph 'BillingService#charge' --format dot | dot -Tsvg -o impact.svg
47
+
48
+ Global options:
49
+ BANNER
50
+
51
+ def initialize(stdout: $stdout, stderr: $stderr)
52
+ @stdout = stdout
53
+ @stderr = stderr
54
+ end
55
+
56
+ # @param argv [Array<String>]
57
+ # @return [Integer] the process exit code
58
+ def run(argv)
59
+ options = { format: "text", verbose: false, quiet: false, no_cache: false }
60
+ rest = parse_global(argv.dup, options)
61
+
62
+ return handle_no_command(options) if rest.empty?
63
+
64
+ name = rest.shift
65
+ command_class = COMMANDS[name]
66
+
67
+ return unknown_command(name) if command_class.nil?
68
+
69
+ command_class.new(options: options, stdout: @stdout, stderr: @stderr).call(rest)
70
+ rescue OptionParser::ParseError => e
71
+ @stderr.puts "Error: #{e.message}"
72
+ @stderr.puts "Run `ripple-effect --help` for usage."
73
+ Command::USER_ERROR
74
+ rescue Interrupt
75
+ @stderr.puts "Interrupted."
76
+ Command::INTERNAL_ERROR
77
+ rescue RippleEffect::Error => e
78
+ @stderr.puts "Error: #{e.message}"
79
+ Command::INTERNAL_ERROR
80
+ end
81
+
82
+ private
83
+
84
+ # Stops at the first non-flag argument so that a subcommand's own flags are
85
+ # left for the subcommand to parse.
86
+ def parse_global(argv, options)
87
+ parser = OptionParser.new do |opts|
88
+ opts.banner = BANNER
89
+ opts.on("--root PATH", "Project root (default: current directory)") { |v| options[:root] = v }
90
+ opts.on("--config PATH", "Path to a .ripple-effect.yml") { |v| options[:config] = v }
91
+ opts.on("--format FORMAT", %w[text json], "Output format: text (default) or json") do |v|
92
+ options[:format] = v
93
+ # `tests` defaults to `paths` rather than `text`, so it needs to know
94
+ # whether the user actually asked for a format or just got the default.
95
+ options[:format_given] = true
96
+ end
97
+ opts.on("--no-cache", "Ignore and do not write the graph cache") { options[:no_cache] = true }
98
+ opts.on("--verbose", "Include stats and every diagnostic") { options[:verbose] = true }
99
+ opts.on("--quiet", "Suppress non-essential output") { options[:quiet] = true }
100
+ opts.on("-v", "--version", "Print the version") { options[:print_version] = true }
101
+ opts.on("-h", "--help", "Show this message") { options[:print_help] = true }
102
+ end
103
+
104
+ parser.order(argv)
105
+ ensure
106
+ @parser = parser
107
+ end
108
+
109
+ def handle_no_command(options)
110
+ if options[:print_version]
111
+ @stdout.puts VERSION
112
+ return Command::SUCCESS
113
+ end
114
+
115
+ @stdout.puts @parser
116
+ options[:print_help] ? Command::SUCCESS : Command::USER_ERROR
117
+ end
118
+
119
+ def unknown_command(name)
120
+ @stderr.puts "Error: unknown command `#{name}`"
121
+ @stderr.puts "Available commands: #{COMMANDS.keys.join(', ')}"
122
+ Command::USER_ERROR
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../project"
5
+ require_relative "../analyzer"
6
+ require_relative "../formatters/text"
7
+ require_relative "../formatters/json"
8
+ require_relative "../formatters/dot"
9
+
10
+ module RippleEffect
11
+ module CLI
12
+ # Shared behaviour for every subcommand: option parsing, project construction,
13
+ # and the exit-code contract.
14
+ #
15
+ # Exit codes are part of the interface:
16
+ # 0 success
17
+ # 1 a threshold the user asked us to enforce was met
18
+ # 2 a query, configuration, or usage error
19
+ # 3 an analyzer or internal failure
20
+ #
21
+ # @abstract Subclasses implement {#run} and {#banner}.
22
+ class Command
23
+ SUCCESS = 0
24
+ THRESHOLD_MET = 1
25
+ USER_ERROR = 2
26
+ INTERNAL_ERROR = 3
27
+
28
+ attr_reader :options, :stdout, :stderr
29
+
30
+ # @param options [Hash] global options already parsed by {Application}
31
+ def initialize(options: {}, stdout: $stdout, stderr: $stderr)
32
+ @options = options
33
+ @stdout = stdout
34
+ @stderr = stderr
35
+ end
36
+
37
+ # @param argv [Array<String>] arguments after the subcommand name
38
+ # @return [Integer] the process exit code
39
+ def call(argv)
40
+ # `--help` unwinds from inside OptionParser, which has no other way to say
41
+ # "we are done, successfully".
42
+ catch(:halt) do
43
+ rest = parse(argv)
44
+ run(rest)
45
+ end
46
+ rescue OptionParser::ParseError => e
47
+ report(e, code: USER_ERROR, error_code: "usage")
48
+ rescue QueryError, ConfigurationError, ProjectError, GitError => e
49
+ report(e, code: USER_ERROR, error_code: user_error_code(e))
50
+ rescue IndexError => e
51
+ report(e, code: INTERNAL_ERROR, error_code: "index_error")
52
+ end
53
+
54
+ # @return [String] the usage banner
55
+ def banner = raise NotImplementedError
56
+
57
+ # @param arguments [Array<String>] positional arguments
58
+ # @return [Integer] exit code
59
+ def run(arguments) = raise NotImplementedError
60
+
61
+ private
62
+
63
+ # Subclasses add their own flags here.
64
+ def define_options(parser); end
65
+
66
+ def parse(argv)
67
+ parser = OptionParser.new do |opts|
68
+ opts.banner = banner
69
+ define_options(opts)
70
+ define_global_options(opts)
71
+ opts.on("-h", "--help", "Show this message") do
72
+ stdout.puts opts
73
+ throw :halt, SUCCESS
74
+ end
75
+ end
76
+
77
+ parser.parse(argv)
78
+ end
79
+
80
+ # The global flags are accepted after the subcommand as well as before it.
81
+ # `ripple-effect inspect Foo --root ../app` is what people actually type, and
82
+ # rejecting it would be a pointless lesson in argument order.
83
+ def define_global_options(parser)
84
+ parser.on("--root PATH", "Project root (default: current directory)") { |v| options[:root] = v }
85
+ parser.on("--config PATH", "Path to a .ripple-effect.yml") { |v| options[:config] = v }
86
+ parser.on("--no-cache", "Ignore and do not write the graph cache") { options[:no_cache] = true }
87
+ parser.on("--verbose", "Include stats and every diagnostic") { options[:verbose] = true }
88
+ parser.on("--quiet", "Suppress non-essential output") { options[:quiet] = true }
89
+
90
+ # `tests` and `graph` define their own --format with different values.
91
+ return if respond_to?(:custom_format_option?, true) && custom_format_option?
92
+
93
+ parser.on("--format FORMAT", %w[text json], "Output format: text (default) or json") do |value|
94
+ options[:format] = value
95
+ options[:format_given] = true
96
+ end
97
+ end
98
+
99
+ # Subclasses that define their own --format override this.
100
+ def custom_format_option? = false
101
+
102
+ def project
103
+ @project ||= Project.new(root: options[:root] || Dir.pwd, config_path: options[:config])
104
+ end
105
+
106
+ def analyzer
107
+ @analyzer ||= Analyzer.new(project: project, cache: !options[:no_cache])
108
+ end
109
+
110
+ def json? = options[:format] == "json"
111
+
112
+ # Prints a result in whichever format the user asked for.
113
+ def emit(result)
114
+ stdout.print(
115
+ if json?
116
+ Formatters::Json.new(result: result).render
117
+ else
118
+ Formatters::Text.new(result: result, verbose: options[:verbose]).render
119
+ end
120
+ )
121
+ end
122
+
123
+ # In JSON mode an error is still JSON, on stderr, so a consumer never has to
124
+ # parse a human sentence.
125
+ def report(error, code:, error_code:)
126
+ if json?
127
+ stderr.print Formatters::Json.error(error, code: error_code)
128
+ else
129
+ stderr.puts "Error: #{error.message}"
130
+ end
131
+
132
+ code
133
+ end
134
+
135
+ def user_error_code(error)
136
+ case error
137
+ when QueryError then "query_error"
138
+ when ConfigurationError then "configuration_error"
139
+ when GitError then "git_error"
140
+ else "project_error"
141
+ end
142
+ end
143
+
144
+ # `--depth 3` / `--depth all`
145
+ def parse_depth(value)
146
+ return nil if value.nil? || value == "all"
147
+
148
+ depth = Integer(value, exception: false)
149
+ raise OptionParser::InvalidArgument, "--depth must be a positive integer or \"all\"" if depth.nil? || depth < 1
150
+
151
+ depth
152
+ end
153
+
154
+ def parse_confidence(value)
155
+ return nil if value.nil?
156
+ unless Confidence.valid?(value)
157
+ raise OptionParser::InvalidArgument,
158
+ "--min-confidence must be high, medium, or low"
159
+ end
160
+
161
+ Confidence.cast(value)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command"
4
+
5
+ module RippleEffect
6
+ module CLI
7
+ # `ripple-effect diff BASE [HEAD]`
8
+ class DiffCommand < Command
9
+ def banner
10
+ <<~BANNER
11
+ Usage: ripple-effect diff BASE [HEAD] [options]
12
+
13
+ Show the blast radius of everything changed between two revisions.
14
+ With only BASE, compares BASE against the working tree, including both
15
+ staged and unstaged changes.
16
+
17
+ Examples:
18
+ ripple-effect diff main
19
+ ripple-effect diff origin/main --format json
20
+ ripple-effect diff v1.2.0 HEAD --fail-on-risk high
21
+
22
+ Options:
23
+ BANNER
24
+ end
25
+
26
+ def run(arguments)
27
+ base, head = arguments
28
+
29
+ if base.nil?
30
+ stderr.puts "Error: diff requires a BASE revision (e.g. `ripple-effect diff main`)"
31
+ return USER_ERROR
32
+ end
33
+
34
+ result = analyzer.diff(
35
+ base: base, head: head,
36
+ depth: @depth_given ? @depth : :default,
37
+ min_confidence: @min_confidence,
38
+ include_low_confidence: @include_low_confidence
39
+ )
40
+
41
+ emit(result)
42
+ exit_code_for(result)
43
+ end
44
+
45
+ private
46
+
47
+ # Analysis finding impact is not a failure. Exiting non-zero happens only
48
+ # when the user asked us to enforce a threshold.
49
+ def exit_code_for(result)
50
+ return SUCCESS unless @fail_on_risk
51
+ return SUCCESS unless result.risk.at_least?(@fail_on_risk)
52
+
53
+ stderr.puts "Risk #{result.risk.level} met the --fail-on-risk #{@fail_on_risk} threshold." unless json?
54
+
55
+ THRESHOLD_MET
56
+ end
57
+
58
+ def define_options(parser)
59
+ parser.on("--depth N", "Maximum hops to follow (integer or \"all\")") do |value|
60
+ @depth = parse_depth(value)
61
+ @depth_given = true
62
+ end
63
+ parser.on("--min-confidence LEVEL", "Only follow edges at least this confident") do |value|
64
+ @min_confidence = parse_confidence(value)
65
+ end
66
+ parser.on("--include-low-confidence", "Also follow low-confidence edges") do
67
+ @include_low_confidence = true
68
+ end
69
+ parser.on("--fail-on-risk LEVEL", %w[low medium high critical],
70
+ "Exit 1 when risk reaches this level") do |value|
71
+ @fail_on_risk = value.to_sym
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command"
4
+
5
+ module RippleEffect
6
+ module CLI
7
+ # `ripple-effect doctor`
8
+ #
9
+ # Reports what RippleEffect can see and what it will do, including what it
10
+ # will never do. Makes no network call, here or anywhere else.
11
+ class DoctorCommand < Command
12
+ def banner
13
+ <<~BANNER
14
+ Usage: ripple-effect doctor [options]
15
+
16
+ Check that Ripple Effect can analyse this project, and report what it found.
17
+
18
+ Options:
19
+ BANNER
20
+ end
21
+
22
+ def run(_arguments)
23
+ report = analyzer.doctor
24
+
25
+ if json?
26
+ stdout.print "#{JSON.pretty_generate(report)}\n"
27
+ return SUCCESS
28
+ end
29
+
30
+ render(report)
31
+ report["ruby_supported"] ? SUCCESS : USER_ERROR
32
+ end
33
+
34
+ private
35
+
36
+ def render(report)
37
+ stdout.puts "Ripple Effect #{VERSION}"
38
+ stdout.puts ""
39
+
40
+ section("Project", [
41
+ ["root", report["root"]],
42
+ ["looks like Rails", yes_no(report["rails_like"])],
43
+ ["config", report["config_path"] || "(defaults; no .ripple-effect.yml)"],
44
+ ["test framework", report["test_framework"]]
45
+ ])
46
+
47
+ section("Environment", [
48
+ ["ruby", ruby_line(report)],
49
+ ["static index", report["backend"]],
50
+ ["git repository", yes_no(report["git_repository"])]
51
+ ])
52
+
53
+ section("Index", [
54
+ ["files indexed", report["files_indexed"]],
55
+ ["templates indexed", report["view_files"]],
56
+ ["engines discovered", engines_line(report)],
57
+ ["test files", report["test_files"]],
58
+ ["graph nodes", report["nodes"]],
59
+ ["graph edges", report["edges"]],
60
+ ["diagnostics", report["diagnostics"]]
61
+ ])
62
+
63
+ section("Cache", [
64
+ ["enabled", yes_no(report["cache_enabled"])],
65
+ ["directory", report["cache_directory"]],
66
+ ["writable", yes_no(report["cache_writable"])]
67
+ ])
68
+
69
+ section("Privacy", [
70
+ ["network access", "never"],
71
+ ["telemetry", "never"],
72
+ ["boots your application", "never"],
73
+ ["evaluates your source", "never"]
74
+ ])
75
+
76
+ return if report["config_warnings"].empty?
77
+
78
+ stdout.puts "Configuration warnings"
79
+ report["config_warnings"].each { |warning| stdout.puts " #{warning}" }
80
+ stdout.puts ""
81
+ end
82
+
83
+ def section(title, rows)
84
+ stdout.puts title
85
+ rows.each { |label, value| stdout.puts " #{label.to_s.ljust(24)} #{value}" }
86
+ stdout.puts ""
87
+ end
88
+
89
+ def ruby_line(report)
90
+ support = report["ruby_supported"] ? "(supported)" : "(unsupported, needs >= 3.2)"
91
+ "#{report['ruby_version']} #{support}"
92
+ end
93
+
94
+ def engines_line(report)
95
+ engines = report["engine_roots"]
96
+ return "none" if engines.empty?
97
+
98
+ "#{engines.length} (#{engines.first(4).join(', ')}#{', ...' if engines.length > 4})"
99
+ end
100
+
101
+ def yes_no(value)
102
+ value ? "yes" : "no"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command"
4
+
5
+ module RippleEffect
6
+ module CLI
7
+ # `ripple-effect graph SYMBOL --format dot`
8
+ class GraphCommand < Command
9
+ def banner
10
+ <<~BANNER
11
+ Usage: ripple-effect graph SYMBOL [options]
12
+
13
+ Emit the impacted subgraph for SYMBOL.
14
+
15
+ Examples:
16
+ ripple-effect graph 'BillingService#charge' --format dot | dot -Tsvg -o impact.svg
17
+ ripple-effect graph User --format json
18
+
19
+ Options:
20
+ BANNER
21
+ end
22
+
23
+ def run(arguments)
24
+ symbol = arguments.first
25
+
26
+ if symbol.nil?
27
+ stderr.puts "Error: graph requires a SYMBOL (e.g. 'BillingService#charge')"
28
+ return USER_ERROR
29
+ end
30
+
31
+ result = analyzer.inspect_symbol(
32
+ symbol, depth: @depth_given ? @depth : :default, path: @path
33
+ )
34
+
35
+ stdout.print(
36
+ if options[:format] == "json"
37
+ Formatters::Json.new(result: result).render
38
+ else
39
+ Formatters::Dot.new(result: result).render
40
+ end
41
+ )
42
+
43
+ SUCCESS
44
+ end
45
+
46
+ private
47
+
48
+ # This command defines its own --format with a different set of values.
49
+ def custom_format_option? = true
50
+
51
+ def define_options(parser)
52
+ parser.on("--format FORMAT", %w[dot json], "dot (default) or json") { |value| options[:format] = value }
53
+ parser.on("--depth N", "Maximum hops to follow") do |value|
54
+ @depth = parse_depth(value)
55
+ @depth_given = true
56
+ end
57
+ parser.on("--path PATH", "Disambiguate a symbol declared in several files") { |value| @path = value }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command"
4
+
5
+ module RippleEffect
6
+ module CLI
7
+ # `ripple-effect inspect SYMBOL`
8
+ class InspectCommand < Command
9
+ def banner
10
+ <<~BANNER
11
+ Usage: ripple-effect inspect SYMBOL [options]
12
+
13
+ Show what may be affected by changing SYMBOL, and why.
14
+
15
+ Examples:
16
+ ripple-effect inspect 'BillingService#charge'
17
+ ripple-effect inspect User.find --depth 2
18
+ ripple-effect inspect app/models/order.rb --format json
19
+
20
+ Options:
21
+ BANNER
22
+ end
23
+
24
+ def run(arguments)
25
+ symbol = arguments.first
26
+
27
+ if symbol.nil?
28
+ stderr.puts "Error: inspect requires a SYMBOL (e.g. 'BillingService#charge')"
29
+ return USER_ERROR
30
+ end
31
+
32
+ result = analyzer.inspect_symbol(
33
+ symbol,
34
+ depth: @depth_given ? @depth : :default,
35
+ path: @path,
36
+ min_confidence: @min_confidence,
37
+ include_low_confidence: @include_low_confidence,
38
+ direction: @direction || :dependents
39
+ )
40
+
41
+ emit(result)
42
+ SUCCESS
43
+ end
44
+
45
+ private
46
+
47
+ def define_options(parser)
48
+ parser.on("--depth N", "Maximum hops to follow (integer or \"all\")") do |value|
49
+ @depth = parse_depth(value)
50
+ @depth_given = true
51
+ end
52
+ parser.on("--path PATH", "Disambiguate a symbol declared in several files") { |value| @path = value }
53
+ parser.on("--min-confidence LEVEL", "Only follow edges at least this confident") do |value|
54
+ @min_confidence = parse_confidence(value)
55
+ end
56
+ parser.on("--include-low-confidence", "Also follow low-confidence edges") do
57
+ @include_low_confidence = true
58
+ end
59
+ parser.on("--direction DIR", %w[dependents dependencies both],
60
+ "dependents (default), dependencies, or both") do |value|
61
+ @direction = value.to_sym
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end