|
| 1 | +require "io/console" |
| 2 | + |
| 3 | +module TypeProf::CLI |
| 4 | + class CLI |
| 5 | + def initialize(argv) |
| 6 | + opt = OptionParser.new |
| 7 | + |
| 8 | + opt.banner = "Usage: #{ opt.program_name } [options] files_or_dirs..." |
| 9 | + |
| 10 | + core_options = {} |
| 11 | + lsp_options = {} |
| 12 | + cli_options = {} |
| 13 | + |
| 14 | + output = nil |
| 15 | + rbs_collection_path = nil |
| 16 | + |
| 17 | + opt.separator "" |
| 18 | + opt.separator "Options:" |
| 19 | + opt.on("-o OUTFILE", "Output to OUTFILE instead of stdout") {|v| output = v } |
| 20 | + opt.on("-q", "--quiet", "Quiet mode") do |
| 21 | + core_options[:display_indicator] = false |
| 22 | + end |
| 23 | + opt.on("-v", "--verbose", "Verbose mode") do |
| 24 | + core_options[:show_errors] = true |
| 25 | + end |
| 26 | + opt.on("--version", "Display typeprof version") { cli_options[:display_version] = true } |
| 27 | + opt.on("--collection PATH", "File path of collection configuration") { |v| rbs_collection_path = v } |
| 28 | + opt.on("--no-collection", "Ignore collection configuration") { rbs_collection_path = :no } |
| 29 | + opt.on("--lsp", "LSP server mode") do |v| |
| 30 | + core_options[:display_indicator] = false |
| 31 | + cli_options[:lsp] = true |
| 32 | + end |
| 33 | + |
| 34 | + opt.separator "" |
| 35 | + opt.separator "Analysis output options:" |
| 36 | + opt.on("--[no-]show-typeprof-version", "Display TypeProf version in a header") {|v| core_options[:output_typeprof_version] = v } |
| 37 | + opt.on("--[no-]show-errors", "Display possible errors found during the analysis") {|v| core_options[:output_diagnostics] = v } |
| 38 | + opt.on("--[no-]show-parameter-names", "Display parameter names for methods") {|v| core_options[:output_parameter_names] = v } |
| 39 | + opt.on("--[no-]show-source-locations", "Display definition source locations for methods") {|v| core_options[:output_source_locations] = v } |
| 40 | + |
| 41 | + opt.separator "" |
| 42 | + opt.separator "Advanced options:" |
| 43 | + opt.on("--[no-]stackprof MODE", /\Acpu|wall|object\z/, "Enable stackprof (for debugging purpose)") {|v| cli_options[:stackprof] = v.to_sym } |
| 44 | + |
| 45 | + opt.separator "" |
| 46 | + opt.separator "LSP options:" |
| 47 | + opt.on("--port PORT", Integer, "Specify a port number to listen for requests on") {|v| lsp_options[:port] = v } |
| 48 | + opt.on("--stdio", "Use stdio for LSP transport") {|v| lsp_options[:stdio] = v } |
| 49 | + |
| 50 | + opt.parse!(argv) |
| 51 | + |
| 52 | + if cli_options[:lsp] && !lsp_options.empty? |
| 53 | + raise OptionParser::InvalidOption.new("lsp options with non-lsp mode") |
| 54 | + end |
| 55 | + |
| 56 | + @core_options = { |
| 57 | + rbs_collection: setup_rbs_collection(rbs_collection_path), |
| 58 | + display_indicator: $stderr.tty?, |
| 59 | + output_typeprof_version: true, |
| 60 | + output_errors: false, |
| 61 | + output_parameter_names: false, |
| 62 | + output_source_locations: false, |
| 63 | + }.merge(core_options) |
| 64 | + |
| 65 | + @lsp_options = { |
| 66 | + port: 0, |
| 67 | + stdio: false, |
| 68 | + }.merge(lsp_options) |
| 69 | + |
| 70 | + @cli_options = { |
| 71 | + argv:, |
| 72 | + output: output ? open(output, "w") : $stdout.dup, |
| 73 | + display_version: false, |
| 74 | + stackprof: nil, |
| 75 | + lsp: false, |
| 76 | + }.merge(cli_options) |
| 77 | + |
| 78 | + rescue OptionParser::InvalidOption, OptionParser::MissingArgument |
| 79 | + puts $! |
| 80 | + exit 1 |
| 81 | + end |
| 82 | + |
| 83 | + def setup_rbs_collection(path) |
| 84 | + return nil if path == :no |
| 85 | + |
| 86 | + unless path |
| 87 | + path = RBS::Collection::Config::PATH.exist? ? RBS::Collection::Config::PATH.to_s : nil |
| 88 | + return nil unless path |
| 89 | + end |
| 90 | + |
| 91 | + if !File.readable?(path) |
| 92 | + raise OptionParser::InvalidOption.new("file not found: #{ path }") |
| 93 | + end |
| 94 | + |
| 95 | + lock_path = RBS::Collection::Config.to_lockfile_path(Pathname(path)) |
| 96 | + if !File.readable?(lock_path) |
| 97 | + raise OptionParser::InvalidOption.new("file not found: #{ lock_path.to_s }; please run 'rbs collection install") |
| 98 | + end |
| 99 | + |
| 100 | + RBS::Collection::Config::Lockfile.from_lockfile(lockfile_path: lock_path, data: YAML.load_file(lock_path)) |
| 101 | + end |
| 102 | + |
| 103 | + attr_reader :core_options, :lsp_options, :cli_options |
| 104 | + |
| 105 | + def run |
| 106 | + core = TypeProf::Core::Service.new(@core_options) |
| 107 | + |
| 108 | + if @cli_options[:lsp] |
| 109 | + run_lsp(core) |
| 110 | + else |
| 111 | + run_cli(core) |
| 112 | + end |
| 113 | + end |
| 114 | + |
| 115 | + def run_lsp(core) |
| 116 | + if @lsp_options[:stdio] |
| 117 | + TypeProf::LSP::Server.start_stdio(core) |
| 118 | + else |
| 119 | + TypeProf::LSP::Server.start_socket(core) |
| 120 | + end |
| 121 | + rescue Exception |
| 122 | + puts $!.detailed_message(highlight: false).gsub(/^/, "---") |
| 123 | + raise |
| 124 | + end |
| 125 | + |
| 126 | + def run_cli(core) |
| 127 | + puts "typeprof #{ TypeProf::VERSION }" if @cli_options[:display_version] |
| 128 | + |
| 129 | + files = find_files |
| 130 | + |
| 131 | + set_profiler do |
| 132 | + output = @cli_options[:output] |
| 133 | + |
| 134 | + core.batch(files, @cli_options[:output]) |
| 135 | + |
| 136 | + output.close |
| 137 | + end |
| 138 | + |
| 139 | + rescue OptionParser::InvalidOption, OptionParser::MissingArgument |
| 140 | + puts $! |
| 141 | + exit 1 |
| 142 | + end |
| 143 | + |
| 144 | + def find_files |
| 145 | + files = [] |
| 146 | + @cli_options[:argv].each do |path| |
| 147 | + if File.directory?(path) |
| 148 | + files.concat(Dir.glob("#{ path }/**/*.{rb,rbs}")) |
| 149 | + elsif File.file?(path) |
| 150 | + files << path |
| 151 | + else |
| 152 | + raise OptionParser::InvalidOption.new("no such file or directory -- #{ path }") |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + if files.empty? |
| 157 | + exit if @cli_options[:display_version] |
| 158 | + raise OptionParser::InvalidOption.new("no input files") |
| 159 | + end |
| 160 | + |
| 161 | + files |
| 162 | + end |
| 163 | + |
| 164 | + def set_profiler |
| 165 | + if @cli_options[:stackprof] |
| 166 | + require "stackprof" |
| 167 | + out = "typeprof-stackprof-#{ @cli_options[:stackprof] }.dump" |
| 168 | + StackProf.start(mode: @cli_options[:stackprof], out: out, raw: true) |
| 169 | + end |
| 170 | + |
| 171 | + yield |
| 172 | + |
| 173 | + ensure |
| 174 | + if @cli_options[:stackprof] && defined?(StackProf) |
| 175 | + StackProf.stop |
| 176 | + StackProf.results |
| 177 | + end |
| 178 | + end |
| 179 | + end |
| 180 | +end |
0 commit comments