diff options
Diffstat (limited to 'lib')
290 files changed, 8931 insertions, 5051 deletions
diff --git a/lib/benchmark.gemspec b/lib/benchmark.gemspec index d6e98db805..35deff8d18 100644 --- a/lib/benchmark.gemspec +++ b/lib/benchmark.gemspec @@ -16,6 +16,8 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/benchmark" spec.licenses = ["Ruby", "BSD-2-Clause"] + spec.required_ruby_version = ">= 2.1.0" + spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage diff --git a/lib/benchmark.rb b/lib/benchmark.rb index 9f43255e42..5072bdc2f3 100644 --- a/lib/benchmark.rb +++ b/lib/benchmark.rb @@ -307,6 +307,10 @@ module Benchmark # # Returns the elapsed real time used to execute the given block. + # The unit of time is seconds. + # + # Benchmark.realtime { "a" * 1_000_000_000 } + # #=> 0.5098029999935534 # def realtime # :yield: r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb index c933ad0471..087f94a9ac 100644 --- a/lib/bundled_gems.rb +++ b/lib/bundled_gems.rb @@ -29,24 +29,19 @@ module Gem::BUNDLED_GEMS "ostruct" => "3.5.0", "pstore" => "3.5.0", "rdoc" => "3.5.0", + "win32ole" => "3.5.0", + "fiddle" => "3.5.0", + "logger" => "3.5.0", + "benchmark" => "3.5.0", + "irb" => "3.5.0", + "reline" => "3.5.0", + # "readline" => "3.5.0", # This is wrapper for reline. We don't warn for this. }.freeze + SINCE_FAST_PATH = SINCE.transform_keys { |g| g.sub(/\A.*\-/, "") }.freeze + EXACT = { - "abbrev" => true, - "base64" => true, - "bigdecimal" => true, - "csv" => true, - "drb" => true, - "getoptlong" => true, - "mutex_m" => true, - "nkf" => true, "kconv" => "nkf", - "observer" => true, - "resolv-replace" => true, - "rinda" => true, - "syslog" => true, - "ostruct" => true, - "pstore" => true, - "rdoc" => true, + "kconv" => "nkf", }.freeze PREFIXED = { @@ -57,6 +52,10 @@ module Gem::BUNDLED_GEMS "syslog" => true, }.freeze + OPTIONAL = { + "fiddle" => true, + }.freeze + WARNED = {} # unfrozen conf = ::RbConfig::CONFIG @@ -74,11 +73,29 @@ module Gem::BUNDLED_GEMS [::Kernel.singleton_class, ::Kernel].each do |kernel_class| kernel_class.send(:alias_method, :no_warning_require, :require) kernel_class.send(:define_method, :require) do |name| - if message = ::Gem::BUNDLED_GEMS.warning?(name, specs: spec_names) # rubocop:disable Style/HashSyntax - warn message, :uplevel => 1 + + message = ::Gem::BUNDLED_GEMS.warning?(name, specs: spec_names) + begin + result = kernel_class.send(:no_warning_require, name) + rescue LoadError => e + result = e + end + + if (result || !OPTIONAL[name]) && message + if ::Gem::BUNDLED_GEMS.uplevel > 0 + Kernel.warn message, uplevel: ::Gem::BUNDLED_GEMS.uplevel + else + Kernel.warn message + end + end + + if result.is_a?(LoadError) + raise result + else + result end - kernel_class.send(:no_warning_require, name) end + if kernel_class == ::Kernel kernel_class.send(:private, :require) else @@ -87,6 +104,36 @@ module Gem::BUNDLED_GEMS end end + def self.uplevel + frame_count = 0 + frames_to_skip = 3 + uplevel = 0 + require_found = false + Thread.each_caller_location do |cl| + frame_count += 1 + if frames_to_skip >= 1 + frames_to_skip -= 1 + next + end + uplevel += 1 + if require_found + if cl.base_label != "require" + return uplevel + end + else + if cl.base_label == "require" + require_found = true + end + end + # Don't show script name when bundle exec and call ruby script directly. + if cl.path.end_with?("bundle") + frame_count = 0 + break + end + end + require_found ? 1 : frame_count - 1 + end + def self.find_gem(path) if !path return @@ -97,12 +144,33 @@ module Gem::BUNDLED_GEMS else return end - EXACT[n] or PREFIXED[n = n[%r[\A[^/]+(?=/)]]] && n + (EXACT[n] || !!SINCE[n]) or PREFIXED[n = n[%r[\A[^/]+(?=/)]]] && n end def self.warning?(name, specs: nil) # name can be a feature name or a file path with String or Pathname feature = File.path(name) + + # irb already has reline as a dependency on gemspec, so we don't want to warn about it. + # We should update this with a more general solution when we have another case. + # ex: Gem.loaded_specs[called_gem].dependencies.any? {|d| d.name == feature } + return false if feature.start_with?("reline") && caller_locations(2, 1)[0].to_s.include?("irb") + + # The actual checks needed to properly identify the gem being required + # are costly (see [Bug #20641]), so we first do a much cheaper check + # to exclude the vast majority of candidates. + if feature.include?("/") + # If requiring $LIBDIR/mutex_m.rb, we check SINCE_FAST_PATH["mutex_m"] + # We'll fail to warn requires for files that are not the entry point + # of the gem, e.g. require "logger/formatter.rb" won't warn. + # But that's acceptable because this warning is best effort, + # and in the overwhelming majority of cases logger.rb will end + # up required. + return unless SINCE_FAST_PATH[File.basename(feature, ".*")] + else + return unless SINCE_FAST_PATH[feature] + end + # bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`, # and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`. name = feature.delete_prefix(ARCHDIR) @@ -136,20 +204,36 @@ module Gem::BUNDLED_GEMS end def self.build_message(gem) - msg = " #{RUBY_VERSION < SINCE[gem] ? "will no longer be" : "is not"} part of the default gems since Ruby #{SINCE[gem]}." + msg = " #{RUBY_VERSION < SINCE[gem] ? "will no longer be" : "is not"} part of the default gems starting from Ruby #{SINCE[gem]}." if defined?(Bundler) - msg += " Add #{gem} to your Gemfile or gemspec." + msg += "\nYou can add #{gem} to your Gemfile or gemspec to silence this warning." - # We detect the gem name from caller_locations. We need to skip 2 frames like: - # lib/ruby/3.3.0+0/bundled_gems.rb:90:in `warning?'", - # lib/ruby/3.3.0+0/bundler/rubygems_integration.rb:247:in `block (2 levels) in replace_require'", + # We detect the gem name from caller_locations. First we walk until we find `require` + # then take the first frame that's not from `require`. # # Additionally, we need to skip Bootsnap and Zeitwerk if present, these # gems decorate Kernel#require, so they are not really the ones issuing # the require call users should be warned about. Those are upwards. - location = Thread.each_caller_location(2) do |cl| - break cl.path unless cl.base_label == "require" + frames_to_skip = 3 + location = nil + require_found = false + Thread.each_caller_location do |cl| + if frames_to_skip >= 1 + frames_to_skip -= 1 + next + end + + if require_found + if cl.base_label != "require" + location = cl.path + break + end + else + if cl.base_label == "require" + require_found = true + end + end end if location && File.file?(location) && !location.start_with?(Gem::BUNDLED_GEMS::LIBDIR) @@ -161,7 +245,7 @@ module Gem::BUNDLED_GEMS end end if caller_gem - msg += " Also contact author of #{caller_gem} to add #{gem} into its gemspec." + msg += "\nAlso please contact the author of #{caller_gem} to request adding #{gem} into its gemspec." end end else @@ -182,7 +266,7 @@ class LoadError name = path.tr("/", "-") if !defined?(Bundler) && Gem::BUNDLED_GEMS::SINCE[name] && !Gem::BUNDLED_GEMS::WARNED[name] - warn name + Gem::BUNDLED_GEMS.build_message(name) + warn name + Gem::BUNDLED_GEMS.build_message(name), uplevel: Gem::BUNDLED_GEMS.uplevel end super end diff --git a/lib/bundler.rb b/lib/bundler.rb index 5033109db6..9b00610ff0 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -42,6 +42,7 @@ module Bundler autoload :Checksum, File.expand_path("bundler/checksum", __dir__) autoload :CLI, File.expand_path("bundler/cli", __dir__) autoload :CIDetector, File.expand_path("bundler/ci_detector", __dir__) + autoload :CompactIndexClient, File.expand_path("bundler/compact_index_client", __dir__) autoload :Definition, File.expand_path("bundler/definition", __dir__) autoload :Dependency, File.expand_path("bundler/dependency", __dir__) autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__) @@ -166,6 +167,10 @@ module Bundler end end + def auto_switch + self_manager.restart_with_locked_bundler_if_needed + end + # Automatically install dependencies if Bundler.settings[:auto_install] exists. # This is set through config cmd `bundle config set --global auto_install 1`. # @@ -357,7 +362,7 @@ module Bundler def settings @settings ||= Settings.new(app_config_path) rescue GemfileNotFound - @settings = Settings.new(Pathname.new(".bundle").expand_path) + @settings = Settings.new end # @return [Hash] Environment present before Bundler was activated @@ -665,7 +670,7 @@ module Bundler rescue ScriptError, StandardError => e msg = "There was an error while loading `#{path.basename}`: #{e.message}" - raise GemspecError, Dsl::DSLError.new(msg, path, e.backtrace, contents) + raise GemspecError, Dsl::DSLError.new(msg, path.to_s, e.backtrace, contents) end def configure_gem_path diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 40f19c7fed..013ffcdeed 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -65,7 +65,7 @@ module Bundler Bundler.reset_settings_and_root! end - Bundler.self_manager.restart_with_locked_bundler_if_needed + Bundler.auto_switch Bundler.settings.set_command_option_if_given :retry, options[:retry] @@ -110,8 +110,8 @@ module Bundler default_task(Bundler.feature_flag.default_cli_command) class_option "no-color", type: :boolean, desc: "Disable colorization in output" - class_option "retry", type: :numeric, aliases: "-r", banner: "NUM", - desc: "Specify the number of times you wish to attempt network commands" + class_option "retry", type: :numeric, aliases: "-r", banner: "NUM", + desc: "Specify the number of times you wish to attempt network commands" class_option "verbose", type: :boolean, desc: "Enable verbose output mode", aliases: "-V" def help(cli = nil) @@ -229,6 +229,8 @@ module Bundler method_option "system", type: :boolean, banner: "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application" method_option "trust-policy", alias: "P", type: :string, banner: "Gem trust policy (like gem install -P). Must be one of " + Bundler.rubygems.security_policy_keys.join("|") + method_option "target-rbconfig", type: :string, banner: "rbconfig.rb for the deployment target platform" + method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group." method_option "with", type: :array, banner: "Include gems that are part of the specified named group." def install @@ -260,15 +262,15 @@ module Bundler method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" method_option "group", aliases: "-g", type: :array, banner: "Update a specific group" method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel" - method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" - method_option "quiet", type: :boolean, banner: "Only output warnings and errors." + method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead" + method_option "quiet", type: :boolean, banner: "Only output warnings and errors." method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)" method_option "redownload", type: :boolean, aliases: "--force", banner: "Force downloading every gem." method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock" method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" - method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" - method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" - method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" + method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" + method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" + method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" method_option "pre", type: :boolean, banner: "Always choose the highest allowed version when updating gems, regardless of prerelease status" method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." @@ -397,11 +399,11 @@ module Bundler end desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" - method_option "all", type: :boolean, - default: Bundler.feature_flag.cache_all?, - banner: "Include all sources (including path and git)." + method_option "all", type: :boolean, + default: Bundler.feature_flag.cache_all?, + banner: "Include all sources (including path and git)." method_option "all-platforms", type: :boolean, banner: "Include gems for all platforms present in the lockfile, not only the current one" - method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)." + method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)." method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" method_option "no-install", type: :boolean, banner: "Don't install the gems, only update the cache." method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache." @@ -550,10 +552,13 @@ module Bundler method_option :rubocop, type: :boolean, desc: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true`." method_option :changelog, type: :boolean, desc: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`." method_option :test, type: :string, lazy_default: Bundler.settings["gem.test"] || "", aliases: "-t", banner: "Use the specified test framework for your library", + enum: %w[rspec minitest test-unit], desc: "Generate a test directory for your library, either rspec, minitest or test-unit. Set a default with `bundle config set --global gem.test (rspec|minitest|test-unit)`." method_option :ci, type: :string, lazy_default: Bundler.settings["gem.ci"] || "", + enum: %w[github gitlab circle], desc: "Generate CI configuration, either GitHub Actions, GitLab CI or CircleCI. Set a default with `bundle config set --global gem.ci (github|gitlab|circle)`" method_option :linter, type: :string, lazy_default: Bundler.settings["gem.linter"] || "", + enum: %w[rubocop standard], desc: "Add a linter and code formatter, either RuboCop or Standard. Set a default with `bundle config set --global gem.linter (rubocop|standard)`" method_option :github_username, type: :string, default: Bundler.settings["gem.github_username"], banner: "Set your username on GitHub", desc: "Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`." @@ -602,7 +607,7 @@ module Bundler end desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", hide: true - method_option "source", type: :string, banner: "Install gem from the given source" + method_option "source", type: :string, banner: "Install gem from the given source" method_option "group", type: :string, banner: "Install gem into a bundler group" def inject(name, version) SharedHelpers.major_deprecation 2, "The `inject` command has been replaced by the `add` command" @@ -612,16 +617,16 @@ module Bundler desc "lock", "Creates a lockfile without installing" method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems" - method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only" - method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system" + method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only" + method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system" method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile" method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to" method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems" method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile" - method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile" - method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version" - method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version" - method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)" + method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile" + method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version" + method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version" + method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)" method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status" method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" @@ -767,13 +772,10 @@ module Bundler return unless SharedHelpers.md5_available? - latest = Fetcher::CompactIndex. - new(nil, Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org")), nil, nil). - send(:compact_index_client). - instance_variable_get(:@cache). - dependencies("bundler"). - map {|d| Gem::Version.new(d.first) }. - max + require_relative "vendored_uri" + remote = Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org")) + cache_path = Bundler.user_cache.join("compact_index", remote.cache_slug) + latest = Bundler::CompactIndexClient.new(cache_path).latest_version("bundler") return unless latest current = Gem::Version.new(VERSION) diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 002d9e1d33..2b300e1783 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -34,7 +34,7 @@ module Bundler end def validate_options! - raise InvalidOption, "You can not specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic] + raise InvalidOption, "You cannot specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic] # raise error when no gems are specified raise InvalidOption, "Please specify gems to add." if gems.empty? diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb index 33d31cdd27..2adf59d5d5 100644 --- a/lib/bundler/cli/check.rb +++ b/lib/bundler/cli/check.rb @@ -17,7 +17,7 @@ module Bundler begin definition.resolve_only_locally! not_installed = definition.missing_specs - rescue GemNotFound, SolveFailure + rescue GemNotFound, GitError, SolveFailure Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies." Bundler.ui.warn "Install missing gems with `bundle install`." exit 1 diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb index 52db5aef68..ad7f31f3d6 100644 --- a/lib/bundler/cli/fund.rb +++ b/lib/bundler/cli/fund.rb @@ -16,7 +16,7 @@ module Bundler deps = if groups.any? Bundler.definition.dependencies_for(groups) else - Bundler.definition.current_dependencies + Bundler.definition.requested_dependencies end fund_info = deps.each_with_object([]) do |dep, arr| diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb index b6571d0e86..fb0a184e5d 100644 --- a/lib/bundler/cli/gem.rb +++ b/lib/bundler/cli/gem.rb @@ -79,7 +79,7 @@ module Bundler ensure_safe_gem_name(name, constant_array) templates = { - "#{Bundler.preferred_gemfile_name}.tt" => Bundler.preferred_gemfile_name, + "Gemfile.tt" => Bundler.preferred_gemfile_name, "lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb", "lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb", "sig/newgem.rbs.tt" => "sig/#{namespaced_path}.rbs", @@ -191,7 +191,10 @@ module Bundler templates.merge!("standard.yml.tt" => ".standard.yml") end - templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe] + if config[:exe] + templates.merge!("exe/newgem.tt" => "exe/#{name}") + executables.push("exe/#{name}") + end if extension == "c" templates.merge!( @@ -275,6 +278,7 @@ module Bundler end def ask_and_set_test_framework + return if skip?(:test) test_framework = options[:test] || Bundler.settings["gem.test"] if test_framework.to_s.empty? @@ -300,6 +304,10 @@ module Bundler test_framework end + def skip?(option) + options.key?(option) && options[option].nil? + end + def hint_text(setting) if Bundler.settings["gem.#{setting}"] == false "Your choice will only be applied to this gem." @@ -310,6 +318,7 @@ module Bundler end def ask_and_set_ci + return if skip?(:ci) ci_template = options[:ci] || Bundler.settings["gem.ci"] if ci_template.to_s.empty? @@ -341,6 +350,7 @@ module Bundler end def ask_and_set_linter + return if skip?(:linter) linter_template = options[:linter] || Bundler.settings["gem.linter"] linter_template = deprecated_rubocop_option if linter_template.nil? diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 6c102d537d..b0b354cf10 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -12,22 +12,31 @@ module Bundler warn_if_root - Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed + if options[:local] + Bundler.self_manager.restart_with_locked_bundler_if_needed + else + Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed + end - Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD + Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform? # Disable color in deployment mode Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] + if target_rbconfig_path = options[:"target-rbconfig"] + Bundler.rubygems.set_target_rbconfig(target_rbconfig_path) + end + check_for_options_conflicts check_trust_policy if options[:deployment] || options[:frozen] || Bundler.frozen_bundle? unless Bundler.default_lockfile.exist? - flag = "--deployment flag" if options[:deployment] - flag ||= "--frozen flag" if options[:frozen] - flag ||= "deployment setting" + flag = "--deployment flag" if options[:deployment] + flag ||= "--frozen flag" if options[:frozen] + flag ||= "deployment setting" if Bundler.settings[:deployment] + flag ||= "frozen setting" if Bundler.settings[:frozen] raise ProductionError, "The #{flag} requires a lockfile. Please make " \ "sure you have checked your #{SharedHelpers.relative_lockfile_path} into version control " \ "before deploying." diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index dac3d2a09a..3f204bdc45 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -15,8 +15,8 @@ module Bundler end print = options[:print] - previous_ui_level = Bundler.ui.level - Bundler.ui.level = "silent" if print + previous_output_stream = Bundler.ui.output_stream + Bundler.ui.output_stream = :stderr if print Bundler::Fetcher.disable_endpoint = options["full-index"] @@ -48,8 +48,8 @@ module Bundler options["add-platform"].each do |platform_string| platform = Gem::Platform.new(platform_string) if platform.to_s == "unknown" - Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \ - "and adding it will likely lead to resolution errors" + Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile." + exit 1 end definition.add_platform(platform) end @@ -68,7 +68,7 @@ module Bundler end end - Bundler.ui.level = previous_ui_level + Bundler.ui.output_stream = previous_output_stream end end end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index ec42e631bb..75fcdca641 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -54,7 +54,7 @@ module Bundler end if options[:parseable] - Bundler.ui.silence(&definition_resolution) + Bundler.ui.progress(&definition_resolution) else definition_resolution.call end @@ -97,28 +97,26 @@ module Bundler } end - if outdated_gems.empty? + relevant_outdated_gems = if options_include_groups + outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems| + contains_group = groups.split(", ").include?(options[:group]) + next unless options[:groups] || contains_group + + gems + end.compact + else + outdated_gems + end + + if relevant_outdated_gems.empty? unless options[:parseable] Bundler.ui.info(nothing_outdated_message) end else - if options_include_groups - relevant_outdated_gems = outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems| - contains_group = groups.split(", ").include?(options[:group]) - next unless options[:groups] || contains_group - - gems - end.compact - - if options[:parseable] - print_gems(relevant_outdated_gems) - else - print_gems_table(relevant_outdated_gems) - end - elsif options[:parseable] - print_gems(outdated_gems) + if options[:parseable] + print_gems(relevant_outdated_gems) else - print_gems_table(outdated_gems) + print_gems_table(relevant_outdated_gems) end exit 1 diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb index 68e0d7e0d5..a4f5bb1638 100644 --- a/lib/bundler/compact_index_client.rb +++ b/lib/bundler/compact_index_client.rb @@ -4,6 +4,29 @@ require "pathname" require "set" module Bundler + # The CompactIndexClient is responsible for fetching and parsing the compact index. + # + # The compact index is a set of caching optimized files that are used to fetch gem information. + # The files are: + # - names: a list of all gem names + # - versions: a list of all gem versions + # - info/[gem]: a list of all versions of a gem + # + # The client is instantiated with: + # - `directory`: the root directory where the cache files are stored. + # - `fetcher`: (optional) an object that responds to #call(uri_path, headers) and returns an http response. + # If the `fetcher` is not provided, the client will only read cached files from disk. + # + # The client is organized into: + # - `Updater`: updates the cached files on disk using the fetcher. + # - `Cache`: calls the updater, caches files, read and return them from disk + # - `Parser`: parses the compact index file data + # - `CacheFile`: a concurrency safe file reader/writer that verifies checksums + # + # The client is intended to optimize memory usage and performance. + # It is called 100s or 1000s of times, parsing files with hundreds of thousands of lines. + # It may be called concurrently without global interpreter lock in some Rubies. + # As a result, some methods may look more complex than necessary to save memory or time. class CompactIndexClient # NOTE: MD5 is here not because we expect a server to respond with it, but # because we use it to generate the etag on first request during the upgrade @@ -12,6 +35,13 @@ module Bundler SUPPORTED_DIGESTS = { "sha-256" => :SHA256, "md5" => :MD5 }.freeze DEBUG_MUTEX = Thread::Mutex.new + # info returns an Array of INFO Arrays. Each INFO Array has the following indices: + INFO_NAME = 0 + INFO_VERSION = 1 + INFO_PLATFORM = 2 + INFO_DEPS = 3 + INFO_REQS = 4 + def self.debug return unless ENV["DEBUG_COMPACT_INDEX"] DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") } @@ -21,106 +51,47 @@ module Bundler require_relative "compact_index_client/cache" require_relative "compact_index_client/cache_file" + require_relative "compact_index_client/parser" require_relative "compact_index_client/updater" - attr_reader :directory - - def initialize(directory, fetcher) - @directory = Pathname.new(directory) - @updater = Updater.new(fetcher) - @cache = Cache.new(@directory) - @endpoints = Set.new - @info_checksums_by_name = {} - @parsed_checksums = false - @mutex = Thread::Mutex.new - end - - def execution_mode=(block) - Bundler::CompactIndexClient.debug { "execution_mode=" } - @endpoints = Set.new - - @execution_mode = block - end - - # @return [Lambda] A lambda that takes an array of inputs and a block, and - # maps the inputs with the block in parallel. - # - def execution_mode - @execution_mode || sequentially - end - - def sequential_execution_mode! - self.execution_mode = sequentially - end - - def sequentially - @sequentially ||= lambda do |inputs, &blk| - inputs.map(&blk) - end + def initialize(directory, fetcher = nil) + @cache = Cache.new(directory, fetcher) + @parser = Parser.new(@cache) end def names - Bundler::CompactIndexClient.debug { "/names" } - update("names", @cache.names_path, @cache.names_etag_path) - @cache.names + Bundler::CompactIndexClient.debug { "names" } + @parser.names end def versions - Bundler::CompactIndexClient.debug { "/versions" } - update("versions", @cache.versions_path, @cache.versions_etag_path) - versions, @info_checksums_by_name = @cache.versions - versions + Bundler::CompactIndexClient.debug { "versions" } + @parser.versions end def dependencies(names) Bundler::CompactIndexClient.debug { "dependencies(#{names})" } - execution_mode.call(names) do |name| - update_info(name) - @cache.dependencies(name).map {|d| d.unshift(name) } - end.flatten(1) + names.map {|name| info(name) } end - def update_and_parse_checksums! - Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" } - return @info_checksums_by_name if @parsed_checksums - update("versions", @cache.versions_path, @cache.versions_etag_path) - @info_checksums_by_name = @cache.checksums - @parsed_checksums = true - end - - private - - def update(remote_path, local_path, local_etag_path) - Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" } - unless synchronize { @endpoints.add?(remote_path) } - Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } - return - end - @updater.update(url(remote_path), local_path, local_etag_path) + def info(name) + Bundler::CompactIndexClient.debug { "info(#{name})" } + @parser.info(name) end - def update_info(name) - Bundler::CompactIndexClient.debug { "update_info(#{name})" } - path = @cache.info_path(name) - unless existing = @info_checksums_by_name[name] - Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" } - return - end - checksum = SharedHelpers.checksum_for_file(path, :MD5) - if checksum == existing - Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" } - return - end - Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" } - update("info/#{name}", path, @cache.info_etag_path(name)) + def latest_version(name) + Bundler::CompactIndexClient.debug { "latest_version(#{name})" } + @parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max end - def url(path) - path + def available? + Bundler::CompactIndexClient.debug { "available?" } + @parser.available? end - def synchronize - @mutex.synchronize { yield } + def reset! + Bundler::CompactIndexClient.debug { "reset!" } + @cache.reset! end end end diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb index 55911fdecf..bedd7f8028 100644 --- a/lib/bundler/compact_index_client/cache.rb +++ b/lib/bundler/compact_index_client/cache.rb @@ -7,114 +7,89 @@ module Bundler class Cache attr_reader :directory - def initialize(directory) + def initialize(directory, fetcher = nil) @directory = Pathname.new(directory).expand_path - info_roots.each {|dir| mkdir(dir) } - mkdir(info_etag_root) + @updater = Updater.new(fetcher) if fetcher + @mutex = Thread::Mutex.new + @endpoints = Set.new + + @info_root = mkdir("info") + @special_characters_info_root = mkdir("info-special-characters") + @info_etag_root = mkdir("info-etags") end def names - lines(names_path) + fetch("names", names_path, names_etag_path) end - def names_path - directory.join("names") + def versions + fetch("versions", versions_path, versions_etag_path) end - def names_etag_path - directory.join("names.etag") - end + def info(name, remote_checksum = nil) + path = info_path(name) - def versions - versions_by_name = Hash.new {|hash, key| hash[key] = [] } - info_checksums_by_name = {} - - lines(versions_path).each do |line| - name, versions_string, info_checksum = line.split(" ", 3) - info_checksums_by_name[name] = info_checksum || "" - versions_string.split(",") do |version| - delete = version.delete_prefix!("-") - version = version.split("-", 2).unshift(name) - if delete - versions_by_name[name].delete(version) - else - versions_by_name[name] << version - end - end + if remote_checksum && remote_checksum != SharedHelpers.checksum_for_file(path, :MD5) + fetch("info/#{name}", path, info_etag_path(name)) + else + Bundler::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum is nil" : "versions index checksum matches local"})" } + read(path) end - - [versions_by_name, info_checksums_by_name] - end - - def versions_path - directory.join("versions") end - def versions_etag_path - directory.join("versions.etag") + def reset! + @mutex.synchronize { @endpoints.clear } end - def checksums - checksums = {} - - lines(versions_path).each do |line| - name, _, checksum = line.split(" ", 3) - checksums[name] = checksum - end - - checksums - end + private - def dependencies(name) - lines(info_path(name)).map do |line| - parse_gem(line) - end - end + def names_path = directory.join("names") + def names_etag_path = directory.join("names.etag") + def versions_path = directory.join("versions") + def versions_etag_path = directory.join("versions.etag") def info_path(name) name = name.to_s + # TODO: converge this into the info_root by hashing all filenames like info_etag_path if /[^a-z0-9_-]/.match?(name) name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}" - info_roots.last.join(name) + @special_characters_info_root.join(name) else - info_roots.first.join(name) + @info_root.join(name) end end def info_etag_path(name) name = name.to_s - info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}") + @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}") end - private - - def mkdir(dir) - SharedHelpers.filesystem_access(dir) do - FileUtils.mkdir_p(dir) + def mkdir(name) + directory.join(name).tap do |dir| + SharedHelpers.filesystem_access(dir) do + FileUtils.mkdir_p(dir) + end end end - def lines(path) - return [] unless path.file? - lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n") - header = lines.index("---") - header ? lines[header + 1..-1] : lines - end + def fetch(remote_path, path, etag_path) + if already_fetched?(remote_path) + Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" } + else + Bundler::CompactIndexClient.debug { "fetching #{remote_path}" } + @updater&.update(remote_path, path, etag_path) + end - def parse_gem(line) - @dependency_parser ||= GemParser.new - @dependency_parser.parse(line) + read(path) end - def info_roots - [ - directory.join("info"), - directory.join("info-special-characters"), - ] + def already_fetched?(remote_path) + @mutex.synchronize { !@endpoints.add?(remote_path) } end - def info_etag_root - directory.join("info-etags") + def read(path) + return unless path.file? + SharedHelpers.filesystem_access(path, :read, &:read) end end end diff --git a/lib/bundler/compact_index_client/cache_file.rb b/lib/bundler/compact_index_client/cache_file.rb index 5988bc91b3..299d683438 100644 --- a/lib/bundler/compact_index_client/cache_file.rb +++ b/lib/bundler/compact_index_client/cache_file.rb @@ -86,11 +86,6 @@ module Bundler end end - # remove this method when we stop generating md5 digests for legacy etags - def md5 - @digests && @digests["md5"] - end - def digests? @digests&.any? end diff --git a/lib/bundler/compact_index_client/parser.rb b/lib/bundler/compact_index_client/parser.rb new file mode 100644 index 0000000000..3276abdd68 --- /dev/null +++ b/lib/bundler/compact_index_client/parser.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Bundler + class CompactIndexClient + class Parser + # `compact_index` - an object responding to #names, #versions, #info(name, checksum), + # returning the file contents as a string + def initialize(compact_index) + @compact_index = compact_index + @info_checksums = nil + @versions_by_name = nil + @available = nil + @gem_parser = nil + end + + def names + lines(@compact_index.names) + end + + def versions + @versions_by_name ||= Hash.new {|hash, key| hash[key] = [] } + @info_checksums = {} + + lines(@compact_index.versions).each do |line| + name, versions_string, checksum = line.split(" ", 3) + @info_checksums[name] = checksum || "" + versions_string.split(",") do |version| + delete = version.delete_prefix!("-") + version = version.split("-", 2).unshift(name) + if delete + @versions_by_name[name].delete(version) + else + @versions_by_name[name] << version + end + end + end + + @versions_by_name + end + + def info(name) + data = @compact_index.info(name, info_checksums[name]) + lines(data).map {|line| gem_parser.parse(line).unshift(name) } + end + + def available? + return @available unless @available.nil? + @available = !info_checksums.empty? + end + + private + + def info_checksums + @info_checksums ||= lines(@compact_index.versions).each_with_object({}) do |line, checksums| + parse_version_checksum(line, checksums) + end + end + + def lines(data) + return [] if data.nil? || data.empty? + lines = data.split("\n") + header = lines.index("---") + header ? lines[header + 1..-1] : lines + end + + def gem_parser + @gem_parser ||= GemParser.new + end + + # This is mostly the same as `split(" ", 3)` but it avoids allocating extra objects. + # This method gets called at least once for every gem when parsing versions. + def parse_version_checksum(line, checksums) + return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactor index files + return unless (checksum_start = line.index(" ", name_end + 1) + 1) + checksum_end = line.size - checksum_start + + line.freeze # allows slicing into the string to not allocate a copy of the line + name = line[0, name_end] + checksum = line[checksum_start, checksum_end] + checksums[name.freeze] = checksum # freeze name since it is used as a hash key + end + end + end +end diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb index 36f6b81db8..88c7146900 100644 --- a/lib/bundler/compact_index_client/updater.rb +++ b/lib/bundler/compact_index_client/updater.rb @@ -28,7 +28,6 @@ module Bundler CacheFile.copy(local_path) do |file| etag = etag_path.read.tap(&:chomp!) if etag_path.file? - etag ||= generate_etag(etag_path, file) # Remove this after 2.5.0 has been out for a while. # Subtract a byte to ensure the range won't be empty. # Avoids 416 (Range Not Satisfiable) responses. @@ -67,16 +66,6 @@ module Bundler etag_path.read.tap(&:chomp!) if etag_path.file? end - # When first releasing this opaque etag feature, we want to generate the old MD5 etag - # based on the content of the file. After that it will always use the saved opaque etag. - # This transparently saves existing users with good caches from updating a bunch of files. - # Remove this behavior after 2.5.0 has been out for a while. - def generate_etag(etag_path, file) - etag = file.md5.hexdigest - CacheFile.write(etag_path, etag) - etag - end - def etag_from_response(response) return unless response["ETag"] etag = response["ETag"].delete_prefix("W/") diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb index de9698b577..9564771e78 100644 --- a/lib/bundler/constants.rb +++ b/lib/bundler/constants.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true +require "rbconfig" + module Bundler WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/ + deprecate_constant :WINDOWS + FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd") - NULL = File::NULL + deprecate_constant :FREEBSD + + NULL = File::NULL + deprecate_constant :NULL end diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 22070b6b17..e7e6c49e6c 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -69,7 +69,6 @@ module Bundler @sources = sources @unlock = unlock @optional_groups = optional_groups - @remote = false @prefer_local = false @specs = nil @ruby_version = ruby_version @@ -82,7 +81,7 @@ module Bundler @resolved_bundler_version = nil @locked_ruby_version = nil - @new_platform = nil + @new_platforms = [] @removed_platform = nil if lockfile_exists? @@ -116,7 +115,7 @@ module Bundler @originally_locked_specs = @locked_specs @locked_sources = [] @locked_platforms = [] - @locked_checksums = nil + @locked_checksums = Bundler.feature_flag.bundler_3_mode? end locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } @@ -138,7 +137,7 @@ module Bundler end @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) - add_current_platform unless Bundler.frozen_bundle? + @current_platform_missing = add_current_platform unless Bundler.frozen_bundle? converge_path_sources_to_gemspec_sources @path_changes = converge_paths @@ -164,37 +163,24 @@ module Bundler end def resolve_only_locally! - @remote = false sources.local_only! resolve end def resolve_with_cache! + sources.local! sources.cached! resolve end def resolve_remotely! - @remote = true + sources.cached! sources.remote! resolve end - def resolution_mode=(options) - if options["local"] - @remote = false - else - @remote = true - @prefer_local = options["prefer-local"] - end - end - - def setup_sources_for_resolve - if @remote == false - sources.cached! - else - sources.remote! - end + def prefer_local! + @prefer_local = true end # For given dependency list returns a SpecSet with Gemspec of all the required @@ -228,6 +214,7 @@ module Bundler @resolve = nil @resolver = nil @resolution_packages = nil + @source_requirements = nil @specs = nil Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})" @@ -310,7 +297,12 @@ module Bundler end end else - Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}" + if lockfile_exists? + Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}" + else + Bundler.ui.debug "Resolving dependencies because there's no lockfile" + end + start_resolution end end @@ -325,7 +317,7 @@ module Bundler def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or_unused = false) if [true, false, nil].include?(file_or_preserve_unknown_sections) - target_lockfile = lockfile || Bundler.default_lockfile + target_lockfile = lockfile preserve_unknown_sections = file_or_preserve_unknown_sections else target_lockfile = file_or_preserve_unknown_sections @@ -376,6 +368,10 @@ module Bundler end def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) + return unless Bundler.frozen_bundle? + + raise ProductionError, "Frozen mode is set, but there's no lockfile" unless lockfile_exists? + added = [] deleted = [] changed = [] @@ -404,7 +400,7 @@ module Bundler changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`" end - reason = change_reason + reason = nothing_changed? ? "some dependencies were deleted from your gemfile" : change_reason msg = String.new msg << "#{reason.capitalize.strip}, but the lockfile can't be updated because frozen mode is set" msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any? @@ -462,8 +458,10 @@ module Bundler end def add_platform(platform) - @new_platform ||= !@platforms.include?(platform) - @platforms |= [platform] + return if @platforms.include?(platform) + + @new_platforms << platform + @platforms << platform end def remove_platform(platform) @@ -479,13 +477,13 @@ module Bundler end end - attr_reader :sources - private :sources - def nothing_changed? + return false unless lockfile_exists? + !@source_changes && !@dependency_changes && - !@new_platform && + !@current_platform_missing && + @new_platforms.empty? && !@path_changes && !@local_changes && !@missing_lockfile_dep && @@ -502,22 +500,22 @@ module Bundler @unlocking end + attr_writer :source_requirements + private + attr_reader :sources + def should_add_extra_platforms? !lockfile_exists? && generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform] end def lockfile_exists? - file_exists?(lockfile) - end - - def file_exists?(file) - file && File.exist?(file) + lockfile && File.exist?(lockfile) end def write_lock(file, preserve_unknown_sections) - return if Definition.no_lock + return if Definition.no_lock || file.nil? contents = to_lock @@ -534,7 +532,7 @@ module Bundler preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) - if file_exists?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) return if Bundler.frozen_bundle? SharedHelpers.filesystem_access(file) { FileUtils.touch(file) } return @@ -568,8 +566,8 @@ module Bundler def resolution_packages @resolution_packages ||= begin last_resolve = converge_locked_specs - remove_invalid_platforms!(current_dependencies) - packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?) + remove_invalid_platforms! + packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local) packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve) packages = additional_base_requirements_to_force_updates(packages) packages @@ -587,7 +585,7 @@ module Bundler if missing_specs.any? missing_specs.each do |s| locked_gem = @locked_specs[s.name].last - next if locked_gem.nil? || locked_gem.version != s.version || !@remote + next if locked_gem.nil? || locked_gem.version != s.version || sources.local_mode? raise GemNotFound, "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \ "no longer be found in that source. That means the author of #{locked_gem} has removed it. " \ "You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \ @@ -606,7 +604,7 @@ module Bundler break if incomplete_specs.empty? Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies") - setup_sources_for_resolve + sources.remote! resolution_packages.delete(incomplete_specs) @resolve = start_resolution specs = resolve.materialize(dependencies) @@ -628,12 +626,23 @@ module Bundler end def start_resolution + local_platform_needed_for_resolvability = @most_specific_non_local_locked_ruby_platform && !@platforms.include?(local_platform) + @platforms << local_platform if local_platform_needed_for_resolvability + add_platform(Gem::Platform::RUBY) if RUBY_ENGINE == "truffleruby" + result = SpecSet.new(resolver.start) @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version - @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms? - result.complete_platforms!(platforms) + if @most_specific_non_local_locked_ruby_platform + if spec_set_incomplete_for_platform?(result, @most_specific_non_local_locked_ruby_platform) + @platforms.delete(@most_specific_non_local_locked_ruby_platform) + elsif local_platform_needed_for_resolvability + @platforms.delete(local_platform) + end + end + + @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms? SpecSet.new(result.for(dependencies, false, @platforms)) end @@ -642,26 +651,6 @@ module Bundler sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source? end - def pin_locally_available_names(source_requirements) - source_requirements.each_with_object({}) do |(name, original_source), new_source_requirements| - local_source = original_source.dup - local_source.local_only! - - new_source_requirements[name] = if local_source.specs.search(name).any? - local_source - else - original_source - end - end - end - - def current_ruby_platform_locked? - return false unless generic_local_platform_is_ruby? - return false if Bundler.settings[:force_ruby_platform] && !@platforms.include?(Gem::Platform::RUBY) - - current_platform_locked? - end - def current_platform_locked? @platforms.any? do |bundle_platform| MatchPlatform.platforms_match?(bundle_platform, local_platform) @@ -669,9 +658,19 @@ module Bundler end def add_current_platform - return if current_ruby_platform_locked? + return if @platforms.include?(local_platform) + + @most_specific_non_local_locked_ruby_platform = find_most_specific_locked_ruby_platform + return if @most_specific_non_local_locked_ruby_platform - add_platform(local_platform) + @platforms << local_platform + true + end + + def find_most_specific_locked_ruby_platform + return unless generic_local_platform_is_ruby? && current_platform_locked? + + most_specific_locked_platform end def change_reason @@ -693,7 +692,8 @@ module Bundler [ [@source_changes, "the list of sources changed"], [@dependency_changes, "the dependencies in your gemfile changed"], - [@new_platform, "you added a new platform to your gemfile"], + [@current_platform_missing, "your lockfile does not include the current platform"], + [@new_platforms.any?, "you added a new platform to your gemfile"], [@path_changes, "the gemspecs for path gems changed"], [@local_changes, "the gemspecs for git local gems changed"], [@missing_lockfile_dep, "your lock file is missing \"#{@missing_lockfile_dep}\""], @@ -957,17 +957,20 @@ module Bundler end def source_requirements + @source_requirements ||= find_source_requirements + end + + def find_source_requirements # Record the specs available in each gem's source, so that those # specs will be available later when the resolver knows where to # look for that gemspec (or its dependencies) source_requirements = if precompute_source_requirements_for_indirect_dependencies? all_requirements = source_map.all_requirements - all_requirements = pin_locally_available_names(all_requirements) if @prefer_local { default: default_source }.merge(all_requirements) else { default: Source::RubygemsAggregate.new(sources, source_map) }.merge(source_map.direct_requirements) end - source_requirements.merge!(source_map.locked_requirements) unless @remote + source_requirements.merge!(source_map.locked_requirements) if nothing_changed? metadata_dependencies.each do |dep| source_requirements[dep.name] = sources.metadata_source end @@ -1038,8 +1041,7 @@ module Bundler def dup_for_full_unlock unlocked_definition = self.class.new(@lockfile, @dependencies, @sources, true, @ruby_version, @optional_groups, @gemfiles) - unlocked_definition.resolution_mode = { "local" => !@remote } - unlocked_definition.setup_sources_for_resolve + unlocked_definition.source_requirements = source_requirements unlocked_definition.gem_version_promoter.tap do |gvp| gvp.level = gem_version_promoter.level gvp.strict = gem_version_promoter.strict @@ -1048,21 +1050,25 @@ module Bundler unlocked_definition end - def remove_invalid_platforms!(dependencies) + def remove_invalid_platforms! return if Bundler.frozen_bundle? platforms.reverse_each do |platform| next if local_platform == platform || - (@new_platform && platforms.last == platform) || + @new_platforms.include?(platform) || @path_changes || @dependency_changes || - !@originally_locked_specs.incomplete_for_platform?(dependencies, platform) + @locked_spec_with_invalid_deps || + !spec_set_incomplete_for_platform?(@originally_locked_specs, platform) remove_platform(platform) - add_current_platform if platform == Gem::Platform::RUBY end end + def spec_set_incomplete_for_platform?(spec_set, platform) + spec_set.incomplete_for_platform?(current_dependencies, platform) + end + def source_map @source_map ||= SourceMap.new(sources, dependencies, @locked_specs) end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 6af80fb31f..aad8759652 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -42,20 +42,20 @@ module Bundler end def eval_gemfile(gemfile, contents = nil) - expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent) - original_gemfile = @gemfile - @gemfile = expanded_gemfile_path - @gemfiles << expanded_gemfile_path - contents ||= Bundler.read_file(@gemfile.to_s) - instance_eval(contents, @gemfile.to_s, 1) - rescue Exception => e # rubocop:disable Lint/RescueException - message = "There was an error " \ - "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \ - "`#{File.basename gemfile.to_s}`: #{e.message}" - - raise DSLError.new(message, gemfile, e.backtrace, contents) - ensure - @gemfile = original_gemfile + with_gemfile(gemfile) do |current_gemfile| + contents ||= Bundler.read_file(current_gemfile) + instance_eval(contents, current_gemfile, 1) + rescue GemfileEvalError => e + message = "There was an error evaluating `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + rescue GemfileError, InvalidArgumentError, InvalidOption, DeprecatedError, ScriptError => e + message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + rescue StandardError => e + raise unless e.backtrace_locations.first.path == current_gemfile + message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}" + raise DSLError.new(message, current_gemfile, e.backtrace, contents) + end end def gemspec(opts = nil) @@ -219,7 +219,7 @@ module Bundler end def github(repo, options = {}) - raise ArgumentError, "GitHub sources require a block" unless block_given? + raise InvalidArgumentError, "GitHub sources require a block" unless block_given? github_uri = @git_sources["github"].call(repo) git_options = normalize_hash(options).merge("uri" => github_uri) git_source = @sources.add_git_source(git_options) @@ -285,6 +285,16 @@ module Bundler private + def with_gemfile(gemfile) + expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent) + original_gemfile = @gemfile + @gemfile = expanded_gemfile_path + @gemfiles << expanded_gemfile_path + yield @gemfile.to_s + ensure + @gemfile = original_gemfile + end + def add_git_sources git_source(:github) do |repo_name| if repo_name =~ GITHUB_PULL_REQUEST_URL @@ -577,7 +587,7 @@ module Bundler return m unless backtrace && dsl_path && contents - trace_line = backtrace.find {|l| l.include?(dsl_path.to_s) } || trace_line + trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line return m unless trace_line line_numer = trace_line.split(":")[1].to_i - 1 return m unless line_numer @@ -603,7 +613,7 @@ module Bundler def parse_line_number_from_description description = self.description - if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/ + if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path}):\d+)/ trace_line = Regexp.last_match[1] description = description.sub(/\n.*\n(\.\.\.)? *\^~+$/, "").sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ") end diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 87cb352efa..201818cc33 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -92,6 +92,17 @@ module Bundler end end + # needed for `bundle fund` + def metadata + if @remote_specification + @remote_specification.metadata + elsif _local_specification + _local_specification.metadata + else + super + end + end + def _local_specification return unless @loaded_from && File.exist?(local_specification_path) eval(File.read(local_specification_path), nil, local_specification_path).tap do |spec| diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index f6cb198e38..074bef6edc 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -120,7 +120,7 @@ module Bundler specs = Bundler.rubygems.find_name(name) out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? end - if (exe = caller.last.split(":").first)&.match? %r{(exe|bin)/bundler?\z} + if (exe = caller_locations.last.absolute_path)&.match? %r{(exe|bin)/bundler?\z} shebang = File.read(exe).lines.first shebang.sub!(/^#!\s*/, "") unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby") diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index b6a11cc721..a0ce739ad7 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -217,17 +217,33 @@ module Bundler end class InsecureInstallPathError < BundlerError - def initialize(path) + def initialize(name, path) + @name = name @path = path end def message - "The installation path is insecure. Bundler cannot continue.\n" \ - "#{@path} is world-writable (without sticky bit).\n" \ - "Bundler cannot safely replace gems in world-writeable directories due to potential vulnerabilities.\n" \ - "Please change the permissions of this directory or choose a different install path." + "Bundler cannot reinstall #{@name} because there's a previous installation of it at #{@path} that is unsafe to remove.\n" \ + "The parent of #{@path} is world-writable and does not have the sticky bit set, making it insecure to remove due to potential vulnerabilities.\n" \ + "Please change the permissions of #{File.dirname(@path)} or choose a different install path." end status_code(38) end + + class CorruptBundlerInstallError < BundlerError + def initialize(loaded_spec) + @loaded_spec = loaded_spec + end + + def message + "The running version of Bundler (#{Bundler::VERSION}) does not match the version of the specification installed for it (#{@loaded_spec.version}). " \ + "This can be caused by reinstalling Ruby without removing previous installation, leaving around an upgraded default version of Bundler. " \ + "Reinstalling Ruby from scratch should fix the problem." + end + + status_code(39) + end + + class InvalidArgumentError < BundlerError; status_code(40); end end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 6288b22dcd..14721623f9 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -3,7 +3,7 @@ require_relative "vendored_persistent" require_relative "vendored_timeout" require "cgi" -require "securerandom" +require_relative "vendored_securerandom" require "zlib" module Bundler @@ -182,7 +182,7 @@ module Bundler agent << " ci/#{cis.join(",")}" if cis.any? # add a random ID so we can consolidate runs server-side - agent << " " << SecureRandom.hex(8) + agent << " " << Gem::SecureRandom.hex(8) # add any user agent strings set in the config extra_ua = Bundler.settings[:user_agent] diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb index db914839b1..6e5656d26a 100644 --- a/lib/bundler/fetcher/compact_index.rb +++ b/lib/bundler/fetcher/compact_index.rb @@ -4,8 +4,6 @@ require_relative "base" require_relative "../worker" module Bundler - autoload :CompactIndexClient, File.expand_path("../compact_index_client", __dir__) - class Fetcher class CompactIndex < Base def self.compact_index_request(method_name) @@ -36,15 +34,8 @@ module Bundler until remaining_gems.empty? log_specs { "Looking up gems #{remaining_gems.inspect}" } - - deps = begin - parallel_compact_index_client.dependencies(remaining_gems) - rescue TooManyRequestsError - @bundle_worker&.stop - @bundle_worker = nil # reset it. Not sure if necessary - serial_compact_index_client.dependencies(remaining_gems) - end - next_gems = deps.flat_map {|d| d[3].flat_map(&:first) }.uniq + deps = fetch_gem_infos(remaining_gems).flatten(1) + next_gems = deps.flat_map {|d| d[CompactIndexClient::INFO_DEPS].flat_map(&:first) }.uniq deps.each {|dep| gem_info << dep } complete_gems.concat(deps.map(&:first)).uniq! remaining_gems = next_gems - complete_gems @@ -61,7 +52,7 @@ module Bundler return nil end # Read info file checksums out of /versions, so we can know if gems are up to date - compact_index_client.update_and_parse_checksums! + compact_index_client.available? rescue CompactIndexClient::Updater::MismatchedChecksumError => e Bundler.ui.debug(e.message) nil @@ -81,20 +72,20 @@ module Bundler end end - def parallel_compact_index_client - compact_index_client.execution_mode = lambda do |inputs, &blk| - func = lambda {|object, _index| blk.call(object) } - worker = bundle_worker(func) - inputs.each {|input| worker.enq(input) } - inputs.map { worker.deq } - end - - compact_index_client + def fetch_gem_infos(names) + in_parallel(names) {|name| compact_index_client.info(name) } + rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down. + @bundle_worker&.stop + @bundle_worker = nil # reset it. Not sure if necessary + compact_index_client.reset! + names.map {|name| compact_index_client.info(name) } end - def serial_compact_index_client - compact_index_client.sequential_execution_mode! - compact_index_client + def in_parallel(inputs, &blk) + func = lambda {|object, _index| blk.call(object) } + worker = bundle_worker(func) + inputs.each {|input| worker.enq(input) } + inputs.map { worker.deq } end def bundle_worker(func = nil) diff --git a/lib/bundler/force_platform.rb b/lib/bundler/force_platform.rb index 249a24ecd1..7af33218cb 100644 --- a/lib/bundler/force_platform.rb +++ b/lib/bundler/force_platform.rb @@ -2,8 +2,6 @@ module Bundler module ForcePlatform - private - # The `:force_ruby_platform` value used by dependencies for resolution, and # by locked specifications for materialization is `false` by default, except # for TruffleRuby. TruffleRuby generally needs to force the RUBY platform diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index d535d54f9b..5ce0ef6280 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -47,7 +47,7 @@ module Bundler built_gem_path = build_gem end - desc "Generate SHA512 checksum if #{name}-#{version}.gem into the checksums directory." + desc "Generate SHA512 checksum of #{name}-#{version}.gem into the checksums directory." task "build:checksum" => "build" do build_checksum(built_gem_path) end diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb index de007523ec..70ccceb491 100644 --- a/lib/bundler/gem_helpers.rb +++ b/lib/bundler/gem_helpers.rb @@ -46,19 +46,26 @@ module Bundler end module_function :platform_specificity_match - def select_best_platform_match(specs, platform) - matching = specs.select {|spec| spec.match_platform(platform) } + def select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false) + matching = if force_ruby + specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! } + else + specs.select {|spec| spec.match_platform(platform) } + end + + if prefer_locked + locked_originally = matching.select {|spec| spec.is_a?(LazySpecification) } + return locked_originally if locked_originally.any? + end sort_best_platform_match(matching, platform) end module_function :select_best_platform_match - def force_ruby_platform(specs) - matching = specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! } - - sort_best_platform_match(matching, Gem::Platform::RUBY) + def select_best_local_platform_match(specs, force_ruby: false) + select_best_platform_match(specs, local_platform, force_ruby: force_ruby).map(&:materialize_for_installation).compact end - module_function :force_ruby_platform + module_function :select_best_local_platform_match def sort_best_platform_match(matching, platform) exact = matching.select {|spec| spec.platform == platform } diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb index 879b481339..c7e93c9ee0 100644 --- a/lib/bundler/injector.rb +++ b/lib/bundler/injector.rb @@ -23,10 +23,7 @@ module Bundler # @param [Pathname] lockfile_path The lockfile in which to inject the new dependency. # @return [Array] def inject(gemfile_path, lockfile_path) - if Bundler.frozen_bundle? - # ensure the lock and Gemfile are synced - Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) - end + Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true) # temporarily unfreeze Bundler.settings.temporary(deployment: false, frozen: false) do diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb index ae4ccf2138..1b12de1d7c 100644 --- a/lib/bundler/inline.rb +++ b/lib/bundler/inline.rb @@ -46,11 +46,9 @@ def gemfile(install = false, options = {}, &gemfile) Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins? builder = Bundler::Dsl.new builder.instance_eval(&gemfile) - builder.check_primary_source_safety Bundler.settings.temporary(deployment: false, frozen: false) do definition = builder.to_definition(nil, true) - def definition.lock(*); end definition.validate_runtime! if install || definition.missing_specs? @@ -62,8 +60,25 @@ def gemfile(install = false, options = {}, &gemfile) end end - runtime = Bundler::Runtime.new(nil, definition) - runtime.setup.require + begin + runtime = Bundler::Runtime.new(nil, definition).setup + rescue Gem::LoadError => e + name = e.name + version = e.requirement.requirements.first[1] + activated_version = Gem.loaded_specs[name].version + + Bundler.ui.info \ + "The #{name} gem was resolved to #{version}, but #{activated_version} was activated by Bundler while installing it, causing a conflict. " \ + "Bundler will now retry resolving with #{activated_version} instead." + + builder.dependencies.delete_if {|d| d.name == name } + builder.instance_eval { gem name, activated_version } + definition = builder.to_definition(nil, true) + + retry + end + + runtime.require end end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 72e5602cc3..b65546a10a 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -69,9 +69,7 @@ module Bundler Bundler.create_bundle_path ProcessLock.lock do - if Bundler.frozen_bundle? - @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) - end + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) if @definition.dependencies.empty? Bundler.ui.warn "The Gemfile specifies no dependencies" @@ -196,9 +194,14 @@ module Bundler # that said, it's a rare situation (other than rake), and parallel # installation is SO MUCH FASTER. so we let people opt in. def install(options) - force = options["force"] + standalone = options[:standalone] + force = options[:force] + local = options[:local] jobs = installation_parallelization(options) - install_in_parallel jobs, options[:standalone], force + spec_installations = ParallelInstaller.call(self, @definition.specs, jobs, standalone, force, local: local) + spec_installations.each do |installation| + post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message? + end end def installation_parallelization(options) @@ -226,24 +229,17 @@ module Bundler end end - def install_in_parallel(size, standalone, force = false) - spec_installations = ParallelInstaller.call(self, @definition.specs, size, standalone, force) - spec_installations.each do |installation| - post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message? - end - end - # returns whether or not a re-resolve was needed def resolve_if_needed(options) - @definition.resolution_mode = options - - if !@definition.unlocking? && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file? - return false if @definition.nothing_changed? && !@definition.missing_specs? + @definition.prefer_local! if options[:"prefer-local"] + + if options[:local] || (@definition.no_resolve_needed? && !@definition.missing_specs?) + @definition.resolve_with_cache! + false + else + @definition.resolve_remotely! + true end - - @definition.setup_sources_for_resolve - - true end def lock diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb index d3bbcc90f5..1da91857bd 100644 --- a/lib/bundler/installer/gem_installer.rb +++ b/lib/bundler/installer/gem_installer.rb @@ -2,14 +2,15 @@ module Bundler class GemInstaller - attr_reader :spec, :standalone, :worker, :force, :installer + attr_reader :spec, :standalone, :worker, :force, :local, :installer - def initialize(spec, installer, standalone = false, worker = 0, force = false) + def initialize(spec, installer, standalone = false, worker = 0, force = false, local = false) @spec = spec @installer = installer @standalone = standalone @worker = worker @force = force + @local = local end def install_from_spec @@ -54,7 +55,7 @@ module Bundler spec.source.install( spec, force: force, - ensure_builtin_gems_cached: standalone, + local: local, build_args: Array(spec_settings), previous_spec: previous_spec, ) diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb index e745088f81..d10e5ec924 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -68,11 +68,12 @@ module Bundler attr_reader :size - def initialize(installer, all_specs, size, standalone, force, skip: nil) + def initialize(installer, all_specs, size, standalone, force, local: false, skip: nil) @installer = installer @size = size @standalone = standalone @force = force + @local = local @specs = all_specs.map {|s| SpecInstallation.new(s) } @specs.each do |spec_install| spec_install.state = :installed if skip.include?(spec_install.name) @@ -127,7 +128,7 @@ module Bundler def do_install(spec_install, worker_num) Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install) gem_installer = Bundler::GemInstaller.new( - spec_install.spec, @installer, @standalone, worker_num, @force + spec_install.spec, @installer, @standalone, worker_num, @force, @local ) success, message = gem_installer.install_from_spec if success diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb index 5331df2e95..cf5993448c 100644 --- a/lib/bundler/installer/standalone.rb +++ b/lib/bundler/installer/standalone.rb @@ -58,9 +58,6 @@ module Bundler else SharedHelpers.relative_path_to(full_path, from: Bundler.root.join(bundler_path)) end - rescue TypeError - error_message = "#{spec.name} #{spec.version} has an invalid gemspec" - raise Gem::InvalidSpecificationException.new(error_message) end def prevent_gem_activation diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index 1e11621e55..8a15e356c4 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -272,7 +272,7 @@ module Bundler end def parse_platform(line) - @platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/ + @platforms << Gem::Platform.new($1.strip) if line =~ /^ (.*)$/ end def parse_bundled_with(line) diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 56a3b6f85c..dae05bd945 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,24 +1,12 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-ADD" "1" "May 2024" "" +.TH "BUNDLE\-ADD" "1" "September 2024" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" -\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT] [\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-skip\-install] [\-\-strict] [\-\-optimistic] +\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-skip\-install] [\-\-strict|\-\-optimistic] .SH "DESCRIPTION" -Adds the named gem to the Gemfile and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\. -.P -Example: -.P -bundle add rails -.P -bundle add rails \-\-version "< 3\.0, > 1\.1" -.P -bundle add rails \-\-version "~> 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development" -.P -bundle add rails \-\-skip\-install -.P -bundle add rails \-\-group "development, test" +Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\. .SH "OPTIONS" .TP \fB\-\-version\fR, \fB\-v\fR @@ -56,4 +44,27 @@ Adds optimistic declaration of version\. .TP \fB\-\-strict\fR Adds strict declaration of version\. - +.SH "EXAMPLES" +.IP "1." 4 +You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\. +.IP +\fBbundle add rails\fR +.IP "2." 4 +You can add the \fBrails\fR gem with version greater than 1\.1 (not including 1\.1) and less than 3\.0\. +.IP +\fBbundle add rails \-\-version "> 1\.1, < 3\.0"\fR +.IP "3." 4 +You can use the \fBhttps://gems\.example\.com\fR custom source and assign the gem to a group\. +.IP +\fBbundle add rails \-\-version "~> 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development"\fR +.IP "4." 4 +The following adds the \fBgem\fR entry to the Gemfile without installing the gem\. You can install gems later via \fBbundle install\fR\. +.IP +\fBbundle add rails \-\-skip\-install\fR +.IP "5." 4 +You can assign the gem to more than one group\. +.IP +\fBbundle add rails \-\-group "development, test"\fR +.IP "" 0 +.SH "SEE ALSO" +Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 37c92e5fcd..8b38c7a248 100644 --- a/lib/bundler/man/bundle-add.1.ronn +++ b/lib/bundler/man/bundle-add.1.ronn @@ -1,26 +1,19 @@ bundle-add(1) -- Add gem to the Gemfile and run bundle install -================================================================ +============================================================== ## SYNOPSIS -`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE] [--path=PATH] [--git=GIT] [--github=GITHUB] [--branch=BRANCH] [--ref=REF] [--skip-install] [--strict] [--optimistic] +`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE] + [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF] + [--skip-install] [--strict|--optimistic] ## DESCRIPTION -Adds the named gem to the Gemfile and run `bundle install`. `bundle install` can be avoided by using the flag `--skip-install`. -Example: - -bundle add rails - -bundle add rails --version "< 3.0, > 1.1" - -bundle add rails --version "~> 5.0.0" --source "https://gems.example.com" --group "development" - -bundle add rails --skip-install - -bundle add rails --group "development, test" +Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. +`bundle install` can be avoided by using the flag `--skip-install`. ## OPTIONS + * `--version`, `-v`: Specify version requirements(s) for the added gem. @@ -56,3 +49,33 @@ bundle add rails --group "development, test" * `--strict`: Adds strict declaration of version. + +## EXAMPLES + +1. You can add the `rails` gem to the Gemfile without any version restriction. + The source of the gem will be the global source. + + `bundle add rails` + +2. You can add the `rails` gem with version greater than 1.1 (not including 1.1) and less than 3.0. + + `bundle add rails --version "> 1.1, < 3.0"` + +3. You can use the `https://gems.example.com` custom source and assign the gem + to a group. + + `bundle add rails --version "~> 5.0.0" --source "https://gems.example.com" --group "development"` + +4. The following adds the `gem` entry to the Gemfile without installing the + gem. You can install gems later via `bundle install`. + + `bundle add rails --skip-install` + +5. You can assign the gem to more than one group. + + `bundle add rails --group "development, test"` + +## SEE ALSO + +[Gemfile(5)](https://bundler.io/man/gemfile.5.html), +[bundle-remove(1)](bundle-remove.1.html) diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index 4ec301951f..56c9966e75 100644 --- a/lib/bundler/man/bundle-binstubs.1 +++ b/lib/bundler/man/bundle-binstubs.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-BINSTUBS" "1" "May 2024" "" +.TH "BUNDLE\-BINSTUBS" "1" "September 2024" "" .SH "NAME" \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index e2da1269e6..d634eef203 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-CACHE" "1" "May 2024" "" +.TH "BUNDLE\-CACHE" "1" "September 2024" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 index dee1af1326..e15a41e4fd 100644 --- a/lib/bundler/man/bundle-check.1 +++ b/lib/bundler/man/bundle-check.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-CHECK" "1" "May 2024" "" +.TH "BUNDLE\-CHECK" "1" "September 2024" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index 7c7f9b5c77..aa5ccf7594 100644 --- a/lib/bundler/man/bundle-clean.1 +++ b/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-CLEAN" "1" "May 2024" "" +.TH "BUNDLE\-CLEAN" "1" "September 2024" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index a207dbbcaa..47104fb5c6 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-CONFIG" "1" "May 2024" "" +.TH "BUNDLE\-CONFIG" "1" "September 2024" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" @@ -307,7 +307,7 @@ Any \fB\.\fR characters in a host name are mapped to a double underscore (\fB__\ .P This means that if you have a gem server named \fBmy\.gem\-host\.com\fR, you'll need to use the \fBBUNDLE_MY__GEM___HOST__COM\fR variable to configure credentials for it through ENV\. .SH "CONFIGURE BUNDLER DIRECTORIES" -Bundler's home, config, cache and plugin directories are able to be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values +Bundler's home, cache and plugin directories and config file can be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values .IP "" 4 .nf BUNDLE_USER_HOME : $HOME/\.bundle diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 7e5f458fb2..1a0ec2a5dc 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -397,7 +397,7 @@ through ENV. ## CONFIGURE BUNDLER DIRECTORIES -Bundler's home, config, cache and plugin directories are able to be configured +Bundler's home, cache and plugin directories and config file can be configured through environment variables. The default location for Bundler's home directory is `~/.bundle`, which all directories inherit from by default. The following outlines the available environment variables and their default values diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index dca18ec43d..f2b2ddaed0 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-CONSOLE" "1" "May 2024" "" +.TH "BUNDLE\-CONSOLE" "1" "September 2024" "" .SH "NAME" \fBbundle\-console\fR \- Deprecated way to open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 index 6489cc07f7..f225d0cd79 100644 --- a/lib/bundler/man/bundle-doctor.1 +++ b/lib/bundler/man/bundle-doctor.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-DOCTOR" "1" "May 2024" "" +.TH "BUNDLE\-DOCTOR" "1" "September 2024" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 index 1548d29670..e16b7bc747 100644 --- a/lib/bundler/man/bundle-exec.1 +++ b/lib/bundler/man/bundle-exec.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-EXEC" "1" "May 2024" "" +.TH "BUNDLE\-EXEC" "1" "September 2024" "" .SH "NAME" \fBbundle\-exec\fR \- Execute a command in the context of the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index 5df7b0ef2f..e6e58cd409 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-GEM" "1" "May 2024" "" +.TH "BUNDLE\-GEM" "1" "September 2024" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" @@ -44,6 +44,8 @@ When Bundler is configured to not generate tests, an interactive prompt will be .IP When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. .IP "\(bu" 4 +\fB\-\-no\-test\fR: Do not use a test framework (overrides \fB\-\-test\fR specified in the global config)\. +.IP "\(bu" 4 \fB\-\-ci\fR, \fB\-\-ci=github\fR, \fB\-\-ci=gitlab\fR, \fB\-\-ci=circle\fR: Specify the continuous integration service that Bundler should use when generating the project\. Acceptable values are \fBgithub\fR, \fBgitlab\fR and \fBcircle\fR\. A configuration file will be generated in the project directory\. Given no option is specified: .IP When Bundler is configured to generate CI files, this defaults to Bundler's global config setting \fBgem\.ci\fR\. @@ -52,6 +54,8 @@ When Bundler is configured to not generate CI files, an interactive prompt will .IP When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. .IP "\(bu" 4 +\fB\-\-no\-ci\fR: Do not use a continuous integration service (overrides \fB\-\-ci\fR specified in the global config)\. +.IP "\(bu" 4 \fB\-\-linter\fR, \fB\-\-linter=rubocop\fR, \fB\-\-linter=standard\fR: Specify the linter and code formatter that Bundler should add to the project's development dependencies\. Acceptable values are \fBrubocop\fR and \fBstandard\fR\. A configuration file will be generated in the project directory\. Given no option is specified: .IP When Bundler is configured to add a linter, this defaults to Bundler's global config setting \fBgem\.linter\fR\. @@ -60,6 +64,8 @@ When Bundler is configured not to add a linter, an interactive prompt will be di .IP When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\. .IP "\(bu" 4 +\fB\-\-no\-linter\fR: Do not add a linter (overrides \fB\-\-linter\fR specified in the global config)\. +.IP "\(bu" 4 \fB\-e\fR, \fB\-\-edit[=EDITOR]\fR: Open the resulting GEM_NAME\.gemspec in EDITOR, or the default editor if not specified\. The default is \fB$BUNDLER_EDITOR\fR, \fB$VISUAL\fR, or \fB$EDITOR\fR\. .IP "" 0 .SH "SEE ALSO" diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn index 46fa2f179f..2d71d8dabe 100644 --- a/lib/bundler/man/bundle-gem.1.ronn +++ b/lib/bundler/man/bundle-gem.1.ronn @@ -76,6 +76,10 @@ configuration file using the following names: the answer will be saved in Bundler's global config for future `bundle gem` use. +* `--no-test`: + Do not use a test framework (overrides `--test` specified in the global + config). + * `--ci`, `--ci=github`, `--ci=gitlab`, `--ci=circle`: Specify the continuous integration service that Bundler should use when generating the project. Acceptable values are `github`, `gitlab` @@ -92,6 +96,10 @@ configuration file using the following names: the answer will be saved in Bundler's global config for future `bundle gem` use. +* `--no-ci`: + Do not use a continuous integration service (overrides `--ci` specified in + the global config). + * `--linter`, `--linter=rubocop`, `--linter=standard`: Specify the linter and code formatter that Bundler should add to the project's development dependencies. Acceptable values are `rubocop` and @@ -108,6 +116,9 @@ configuration file using the following names: the answer will be saved in Bundler's global config for future `bundle gem` use. +* `--no-linter`: + Do not add a linter (overrides `--linter` specified in the global config). + * `-e`, `--edit[=EDITOR]`: Open the resulting GEM_NAME.gemspec in EDITOR, or the default editor if not specified. The default is `$BUNDLER_EDITOR`, `$VISUAL`, or `$EDITOR`. diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 index a3e7c7770d..d7a05f824e 100644 --- a/lib/bundler/man/bundle-help.1 +++ b/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-HELP" "1" "May 2024" "" +.TH "BUNDLE\-HELP" "1" "September 2024" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index a3d7ff0988..6b401a57f4 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-INFO" "1" "May 2024" "" +.TH "BUNDLE\-INFO" "1" "September 2024" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index a0edaaa18f..f2e444c7c2 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-INIT" "1" "May 2024" "" +.TH "BUNDLE\-INIT" "1" "September 2024" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-inject.1 b/lib/bundler/man/bundle-inject.1 index 7a1038206e..8eb3633837 100644 --- a/lib/bundler/man/bundle-inject.1 +++ b/lib/bundler/man/bundle-inject.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-INJECT" "1" "May 2024" "" +.TH "BUNDLE\-INJECT" "1" "September 2024" "" .SH "NAME" \fBbundle\-inject\fR \- Add named gem(s) with version requirements to Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index cc46a03b7f..7539d18f81 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-INSTALL" "1" "May 2024" "" +.TH "BUNDLE\-INSTALL" "1" "September 2024" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 index 21723608cc..5cbb1c3cfe 100644 --- a/lib/bundler/man/bundle-list.1 +++ b/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-LIST" "1" "May 2024" "" +.TH "BUNDLE\-LIST" "1" "September 2024" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index 8b81b7732f..5f0d43a9aa 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-LOCK" "1" "May 2024" "" +.TH "BUNDLE\-LOCK" "1" "September 2024" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index 41a8804a09..fb5ff1fee7 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-OPEN" "1" "May 2024" "" +.TH "BUNDLE\-OPEN" "1" "September 2024" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index 838fce45cd..ea3005dd87 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-OUTDATED" "1" "May 2024" "" +.TH "BUNDLE\-OUTDATED" "1" "September 2024" "" .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 index 0dbce7a7a4..c3058175fc 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-PLATFORM" "1" "May 2024" "" +.TH "BUNDLE\-PLATFORM" "1" "September 2024" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index 1290abb3ba..34437d9973 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-PLUGIN" "1" "May 2024" "" +.TH "BUNDLE\-PLUGIN" "1" "September 2024" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 index bf4a7cd323..103c6f68ae 100644 --- a/lib/bundler/man/bundle-pristine.1 +++ b/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-PRISTINE" "1" "May 2024" "" +.TH "BUNDLE\-PRISTINE" "1" "September 2024" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 index c3c96b416d..4a2ed4eb13 100644 --- a/lib/bundler/man/bundle-remove.1 +++ b/lib/bundler/man/bundle-remove.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-REMOVE" "1" "May 2024" "" +.TH "BUNDLE\-REMOVE" "1" "September 2024" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index c054875efd..dfbb439218 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-SHOW" "1" "May 2024" "" +.TH "BUNDLE\-SHOW" "1" "September 2024" "" .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index acd80ec75c..5eb9514f03 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-UPDATE" "1" "May 2024" "" +.TH "BUNDLE\-UPDATE" "1" "September 2024" "" .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 index 44eaf92224..a29858181a 100644 --- a/lib/bundler/man/bundle-version.1 +++ b/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-VERSION" "1" "May 2024" "" +.TH "BUNDLE\-VERSION" "1" "September 2024" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-viz.1 b/lib/bundler/man/bundle-viz.1 index 77b902214c..9609e098dd 100644 --- a/lib/bundler/man/bundle-viz.1 +++ b/lib/bundler/man/bundle-viz.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-VIZ" "1" "May 2024" "" +.TH "BUNDLE\-VIZ" "1" "September 2024" "" .SH "NAME" \fBbundle\-viz\fR \- Generates a visual dependency graph for your Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index 199d1ce9fd..d84d788748 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE" "1" "May 2024" "" +.TH "BUNDLE" "1" "September 2024" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index fa9e31f615..f24a1c540d 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "GEMFILE" "5" "May 2024" "" +.TH "GEMFILE" "5" "September 2024" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" @@ -216,6 +216,8 @@ The following platform values are deprecated and should be replaced with \fBwind .IP "\(bu" 4 \fBmswin\fR, \fBmswin64\fR, \fBmingw32\fR, \fBx64_mingw\fR .IP "" 0 +.P +Note that, while unfortunately using the same terminology, the values of this option are different from the values that \fBbundle lock \-\-add\-platform\fR can take\. The values of this option are more closer to "Ruby Implementation" while the values that \fBbundle lock \-\-add\-platform\fR understands are more related to OS and architecture of the different systems where your lockfile will be used\. .SS "FORCE_RUBY_PLATFORM" If you always want the pure ruby variant of a gem to be chosen over platform specific variants, you can use the \fBforce_ruby_platform\fR option: .IP "" 4 diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index 7c1e00d13a..802549737e 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -242,6 +242,12 @@ The following platform values are deprecated and should be replaced with `window * `mswin`, `mswin64`, `mingw32`, `x64_mingw` +Note that, while unfortunately using the same terminology, the values of this +option are different from the values that `bundle lock --add-platform` can take. +The values of this option are more closer to "Ruby Implementation" while the +values that `bundle lock --add-platform` understands are more related to OS and +architecture of the different systems where your lockfile will be used. + ### FORCE_RUBY_PLATFORM If you always want the pure ruby variant of a gem to be chosen over platform diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb index 8563ee358a..690f379389 100644 --- a/lib/bundler/plugin/api/source.rb +++ b/lib/bundler/plugin/api/source.rb @@ -131,7 +131,7 @@ module Bundler Bundler::Index.build do |index| files.each do |file| next unless spec = Bundler.load_gemspec(file) - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION spec.source = self Bundler.rubygems.validate(spec) @@ -196,6 +196,7 @@ module Bundler FileUtils.rm_rf(new_cache_path) FileUtils.cp_r(install_path, new_cache_path) + FileUtils.rm_rf(app_cache_path.join(".git")) FileUtils.touch(app_cache_path.join(".bundlecache")) end diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 1a6711ea6f..a38b6974f8 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -79,13 +79,14 @@ module Bundler def solve_versions(root:, logger:) solver = PubGrub::VersionSolver.new(source: self, root: root, logger: logger) result = solver.solve - result.map {|package, version| version.to_specs(package) }.flatten.uniq + resolved_specs = result.map {|package, version| version.to_specs(package) }.flatten + resolved_specs |= @base.specs_compatible_with(SpecSet.new(resolved_specs)) rescue PubGrub::SolveFailure => e incompatibility = e.incompatibility - names_to_unlock, names_to_allow_prereleases_for, extended_explanation = find_names_to_relax(incompatibility) + names_to_unlock, names_to_allow_prereleases_for, names_to_allow_remote_specs_for, extended_explanation = find_names_to_relax(incompatibility) - names_to_relax = names_to_unlock + names_to_allow_prereleases_for + names_to_relax = names_to_unlock + names_to_allow_prereleases_for + names_to_allow_remote_specs_for if names_to_relax.any? if names_to_unlock.any? @@ -95,11 +96,17 @@ module Bundler end if names_to_allow_prereleases_for.any? - Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retrying considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true + Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retry considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true @base.include_prereleases(names_to_allow_prereleases_for) end + if names_to_allow_remote_specs_for.any? + Bundler.ui.debug "Found conflicts with local versions of #{names_to_allow_remote_specs_for.join(", ")}. Will retry considering remote versions...", true + + @base.include_remote_specs(names_to_allow_remote_specs_for) + end + root, logger = setup_solver Bundler.ui.debug "Retrying resolution...", true @@ -119,6 +126,7 @@ module Bundler def find_names_to_relax(incompatibility) names_to_unlock = [] names_to_allow_prereleases_for = [] + names_to_allow_remote_specs_for = [] extended_explanation = nil while incompatibility.conflict? @@ -133,6 +141,8 @@ module Bundler names_to_unlock << name elsif package.ignores_prereleases? && @all_specs[name].any? {|s| s.version.prerelease? } names_to_allow_prereleases_for << name + elsif package.prefer_local? && @all_specs[name].any? {|s| !s.is_a?(StubSpecification) } + names_to_allow_remote_specs_for << name end no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) } @@ -142,7 +152,7 @@ module Bundler end end - [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, extended_explanation] + [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, names_to_allow_remote_specs_for.uniq, extended_explanation] end def parse_dependency(package, dependency) @@ -243,7 +253,7 @@ module Bundler def all_versions_for(package) name = package.name - results = (@base[name] + filter_prereleases(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] } + results = (@base[name] + filter_specs(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] } if name == "bundler" && !bundler_pinned_to_current_version? bundler_spec = Gem.loaded_specs["bundler"] @@ -254,7 +264,7 @@ module Bundler results = filter_matching_specs(results, locked_requirement) if locked_requirement results.group_by(&:version).reduce([]) do |groups, (version, specs)| - platform_specs = package.platforms.map {|platform| select_best_platform_match(specs, platform) } + platform_specs = package.platform_specs(specs) # If package is a top-level dependency, # candidate is only valid if there are matching versions for all resolution platforms. @@ -269,14 +279,22 @@ module Bundler next groups if platform_specs.all?(&:empty?) end - platform_specs.flatten! - ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY) - groups << Resolver::Candidate.new(version, specs: ruby_specs) if ruby_specs.any? + ruby_group = Resolver::SpecGroup.new(ruby_specs) + + unless ruby_group.empty? + platform_specs.each do |specs| + ruby_group.merge(Resolver::SpecGroup.new(specs)) + end + + groups << Resolver::Candidate.new(version, group: ruby_group, priority: -1) + next groups if package.force_ruby_platform? + end - next groups if platform_specs == ruby_specs || package.force_ruby_platform? + platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq) + next groups if platform_group == ruby_group - groups << Resolver::Candidate.new(version, specs: platform_specs) + groups << Resolver::Candidate.new(version, group: platform_group, priority: 1) groups end @@ -359,12 +377,22 @@ module Bundler end end + def filter_specs(specs, package) + filter_remote_specs(filter_prereleases(specs, package), package) + end + def filter_prereleases(specs, package) return specs unless package.ignores_prereleases? && specs.size > 1 specs.reject {|s| s.version.prerelease? } end + def filter_remote_specs(specs, package) + return specs unless package.prefer_local? + + specs.select {|s| s.is_a?(StubSpecification) } + end + # Ignore versions that depend on themselves incorrectly def filter_invalid_self_dependencies(specs, name) specs.reject do |s| @@ -396,10 +424,13 @@ module Bundler dep_range = dep_constraint.range versions = select_sorted_versions(dep_package, dep_range) - if versions.empty? && dep_package.ignores_prereleases? - @all_versions.delete(dep_package) - @sorted_versions.delete(dep_package) - dep_package.consider_prereleases! + if versions.empty? + if dep_package.ignores_prereleases? || dep_package.prefer_local? + @all_versions.delete(dep_package) + @sorted_versions.delete(dep_package) + end + dep_package.consider_prereleases! if dep_package.ignores_prereleases? + dep_package.consider_remote_versions! if dep_package.prefer_local? versions = select_sorted_versions(dep_package, dep_range) end @@ -431,8 +462,8 @@ module Bundler def requirement_to_range(requirement) ranges = requirement.requirements.map do |(op, version)| - ver = Resolver::Candidate.new(version).generic! - platform_ver = Resolver::Candidate.new(version).platform_specific! + ver = Resolver::Candidate.new(version, priority: -1) + platform_ver = Resolver::Candidate.new(version, priority: 1) case op when "~>" diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb index ad19eeb3f4..3f2436672a 100644 --- a/lib/bundler/resolver/base.rb +++ b/lib/bundler/resolver/base.rb @@ -30,6 +30,10 @@ module Bundler end.compact end + def specs_compatible_with(result) + @base.specs_compatible_with(result) + end + def [](name) @base[name] end @@ -68,6 +72,12 @@ module Bundler end end + def include_remote_specs(names) + names.each do |name| + get_package(name).consider_remote_versions! + end + end + private def indirect_pins(names) diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb index 9e8b913335..f593fc5d61 100644 --- a/lib/bundler/resolver/candidate.rb +++ b/lib/bundler/resolver/candidate.rb @@ -24,10 +24,10 @@ module Bundler attr_reader :version - def initialize(version, specs: []) - @spec_group = Resolver::SpecGroup.new(specs) + def initialize(version, group: nil, priority: -1) + @spec_group = group || SpecGroup.new([]) @version = Gem::Version.new(version) - @ruby_only = specs.map(&:platform).uniq == [Gem::Platform::RUBY] + @priority = priority end def dependencies @@ -40,18 +40,6 @@ module Bundler @spec_group.to_specs(package.force_ruby_platform?) end - def generic! - @ruby_only = true - - self - end - - def platform_specific! - @ruby_only = false - - self - end - def prerelease? @version.prerelease? end @@ -61,7 +49,7 @@ module Bundler end def sort_obj - [@version, @ruby_only ? -1 : 1] + [@version, @priority] end def <=>(other) diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb index 0461328683..5aecc12d05 100644 --- a/lib/bundler/resolver/package.rb +++ b/lib/bundler/resolver/package.rb @@ -15,7 +15,7 @@ module Bundler class Package attr_reader :name, :platforms, :dependency, :locked_version - def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, dependency: nil) + def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil) @name = name @platforms = platforms @locked_version = locked_specs[name].first&.version @@ -23,6 +23,11 @@ module Bundler @dependency = dependency || Dependency.new(name, @locked_version) @top_level = !dependency.nil? @prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore + @prefer_local = prefer_local + end + + def platform_specs(specs) + platforms.map {|platform| GemHelpers.select_best_platform_match(specs, platform, prefer_locked: !unlock?) } end def to_s @@ -65,6 +70,14 @@ module Bundler @prerelease = :consider_last end + def prefer_local? + @prefer_local + end + + def consider_remote_versions! + @prefer_local = false + end + def force_ruby_platform? @dependency.force_ruby_platform end diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb index 5cee444e5e..f950df6eda 100644 --- a/lib/bundler/resolver/spec_group.rb +++ b/lib/bundler/resolver/spec_group.rb @@ -3,6 +3,8 @@ module Bundler class Resolver class SpecGroup + attr_reader :specs + def initialize(specs) @specs = specs end @@ -38,17 +40,33 @@ module Bundler def dependencies @dependencies ||= @specs.map do |spec| __dependencies(spec) + metadata_dependencies(spec) - end.flatten.uniq + end.flatten.uniq.sort + end + + def ==(other) + sorted_spec_names == other.sorted_spec_names + end + + def merge(other) + return false unless equivalent?(other) + + @specs |= other.specs + + true end protected def sorted_spec_names - @sorted_spec_names ||= @specs.map(&:full_name).sort + @specs.map(&:full_name).sort end private + def equivalent?(other) + name == other.name && version == other.version && source == other.source && dependencies == other.dependencies + end + def exemplary_spec @specs.first end diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb index b95c42c361..090cb7e2ca 100644 --- a/lib/bundler/retry.rb +++ b/lib/bundler/retry.rb @@ -50,7 +50,7 @@ module Bundler end return true unless name Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this - Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", Bundler.ui.debug? + Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true end def keep_trying? diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb index 7e9e072b83..0ed5cbc6ca 100644 --- a/lib/bundler/ruby_version.rb +++ b/lib/bundler/ruby_version.rb @@ -23,7 +23,13 @@ module Bundler # specified must match the version. @versions = Array(versions).map do |v| - op, v = Gem::Requirement.parse(normalize_version(v)) + normalized_v = normalize_version(v) + + unless Gem::Requirement::PATTERN.match?(normalized_v) + raise InvalidArgumentError, "#{v} is not a valid requirement on the Ruby version" + end + + op, v = Gem::Requirement.parse(normalized_v) op == "=" ? v.to_s : "#{op} #{v}" end diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index a7539f4adb..0f0680560a 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true -require "pathname" - require "rubygems" unless defined?(Gem) -require "rubygems/specification" - # We can't let `Gem::Source` be autoloaded in the `Gem::Specification#source` # redefinition below, so we need to load it upfront. The reason is that if # Bundler monkeypatches are loaded before RubyGems activates an executable (for @@ -17,10 +13,6 @@ require "rubygems/specification" # `Gem::Source` from the redefined `Gem::Specification#source`. require "rubygems/source" -require_relative "match_metadata" -require_relative "force_platform" -require_relative "match_platform" - # Cherry-pick fixes to `Gem.ruby_version` to be useful for modern Bundler # versions and ignore patchlevels # (https://github.com/rubygems/rubygems/pull/5472, @@ -31,7 +23,60 @@ unless Gem.ruby_version.to_s == RUBY_VERSION || RUBY_PATCHLEVEL == -1 end module Gem + # Can be removed once RubyGems 3.5.11 support is dropped + unless Gem.respond_to?(:freebsd_platform?) + def self.freebsd_platform? + RbConfig::CONFIG["host_os"].to_s.include?("bsd") + end + end + + # Can be removed once RubyGems 3.5.18 support is dropped + unless Gem.respond_to?(:open_file_with_lock) + class << self + remove_method :open_file_with_flock if Gem.respond_to?(:open_file_with_flock) + + def open_file_with_flock(path, &block) + mode = IO::RDONLY | IO::APPEND | IO::CREAT | IO::BINARY + mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE) + + File.open(path, mode) do |io| + begin + io.flock(File::LOCK_EX) + rescue Errno::ENOSYS, Errno::ENOTSUP + rescue Errno::ENOLCK # NFS + raise unless Thread.main == Thread.current + end + yield io + end + end + + def open_file_with_lock(path, &block) + file_lock = "#{path}.lock" + open_file_with_flock(file_lock, &block) + ensure + FileUtils.rm_f file_lock + end + end + end + + require "rubygems/specification" + + # Can be removed once RubyGems 3.5.14 support is dropped + VALIDATES_FOR_RESOLUTION = Specification.new.respond_to?(:validate_for_resolution).freeze + + # Can be removed once RubyGems 3.3.15 support is dropped + FLATTENS_REQUIRED_PATHS = Specification.new.respond_to?(:flatten_require_paths).freeze + class Specification + # Can be removed once RubyGems 3.5.15 support is dropped + correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys + unless @@array_attributes == correct_array_attributes + @@array_attributes = correct_array_attributes # rubocop:disable Style/ClassVars + end + + require_relative "match_metadata" + require_relative "match_platform" + include ::Bundler::MatchMetadata include ::Bundler::MatchPlatform @@ -48,7 +93,7 @@ module Gem def full_gem_path if source.respond_to?(:root) - Pathname.new(loaded_from).dirname.expand_path(source.root).to_s + File.expand_path(File.dirname(loaded_from), source.root) else rg_full_gem_path end @@ -78,11 +123,6 @@ module Gem end end - remove_method :gem_dir - def gem_dir - full_gem_path - end - unless const_defined?(:LATEST_RUBY_WITHOUT_PATCH_VERSIONS) LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1") @@ -127,6 +167,33 @@ module Gem !default_gem? && !File.directory?(full_gem_path) end + unless VALIDATES_FOR_RESOLUTION + def validate_for_resolution + SpecificationPolicy.new(self).validate_for_resolution + end + end + + unless FLATTENS_REQUIRED_PATHS + def flatten_require_paths + return unless raw_require_paths.first.is_a?(Array) + + warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this" + raw_require_paths.flatten! + end + + class << self + module RequirePathFlattener + def from_yaml(input) + spec = super(input) + spec.flatten_require_paths + spec + end + end + + prepend RequirePathFlattener + end + end + private def dependencies_to_gemfile(dependencies, group = nil) @@ -146,41 +213,47 @@ module Gem end end + unless VALIDATES_FOR_RESOLUTION + class SpecificationPolicy + def validate_for_resolution + validate_required! + end + end + end + module BetterPermissionError def data - Bundler::SharedHelpers.filesystem_access(loaded_from, :read) do - super - end + super + rescue Errno::EACCES + raise Bundler::PermissionError.new(loaded_from, :read) end end + require "rubygems/stub_specification" + class StubSpecification prepend BetterPermissionError end class Dependency + require_relative "force_platform" + include ::Bundler::ForcePlatform + attr_reader :force_ruby_platform + attr_accessor :source, :groups alias_method :eql?, :== - def force_ruby_platform - return @force_ruby_platform if defined?(@force_ruby_platform) && !@force_ruby_platform.nil? - - @force_ruby_platform = default_force_ruby_platform - end - - def encode_with(coder) - to_yaml_properties.each do |ivar| - coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar) + unless method_defined?(:encode_with, false) + def encode_with(coder) + [:@name, :@requirement, :@type, :@prerelease, :@version_requirements].each do |ivar| + coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar) + end end end - def to_yaml_properties - instance_variables.reject {|p| ["@source", "@groups"].include?(p.to_s) } - end - def to_lock out = String.new(" #{name}") unless requirement.none? @@ -233,7 +306,7 @@ module Gem # cpu ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu || - (@cpu == "arm" && other.cpu.start_with?("arm"))) && + (@cpu == "arm" && other.cpu.start_with?("armv"))) && # os @os == other.os && @@ -337,4 +410,23 @@ module Gem end end end + + unless Gem.rubygems_version >= Gem::Version.new("3.5.19") + class Resolver::ActivationRequest + remove_method :installed? + + def installed? + case @spec + when Gem::Resolver::VendorSpecification then + true + else + this_spec = full_spec + + Gem::Specification.any? do |s| + s == this_spec && s.base_dir == this_spec.base_dir + end + end + end + end + end end diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index d563868cd2..62756680e7 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -29,7 +29,10 @@ module Bundler write_build_info_file run_post_build_hooks - generate_bin + SharedHelpers.filesystem_access(bin_dir, :write) do + generate_bin + end + generate_plugins write_spec @@ -45,7 +48,17 @@ module Bundler spec end - def pre_install_checks + if Bundler.rubygems.provides?("< 3.5") + def pre_install_checks + super + rescue Gem::FilePermissionError + # Ignore permission checks in RubyGems. Instead, go on, and try to write + # for real. We properly handle permission errors when they happen. + nil + end + end + + def ensure_writable_dir(dir) super rescue Gem::FilePermissionError # Ignore permission checks in RubyGems. Instead, go on, and try to write @@ -68,6 +81,26 @@ module Bundler end end + if Bundler.rubygems.provides?("< 3.5.19") + def generate_bin_script(filename, bindir) + bin_script_path = File.join bindir, formatted_program_filename(filename) + + Gem.open_file_with_lock(bin_script_path) do + require "fileutils" + FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers + + File.open(bin_script_path, "wb", 0o755) do |file| + file.write app_script_text(filename) + file.chmod(options[:prog_mode] || 0o755) + end + end + + verbose bin_script_path + + generate_windows_script filename, bindir + end + end + def build_extensions extension_cache_path = options[:bundler_extension_cache_path] extension_dir = spec.extension_dir @@ -117,12 +150,13 @@ module Bundler def strict_rm_rf(dir) return unless File.exist?(dir) + return if Dir.empty?(dir) parent = File.dirname(dir) parent_st = File.stat(parent) if parent_st.world_writable? && !parent_st.sticky? - raise InsecureInstallPathError.new(parent) + raise InsecureInstallPathError.new(spec.full_name, dir) end begin diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index 494030eab2..b98d3b7b8c 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -34,6 +34,10 @@ module Bundler Gem::Command.build_args = args end + def set_target_rbconfig(path) + Gem.set_target_rbconfig(path) + end + def loaded_specs(name) Gem.loaded_specs[name] end @@ -48,7 +52,7 @@ module Bundler end def validate(spec) - Bundler.ui.silence { spec.validate(false) } + Bundler.ui.silence { spec.validate_for_resolution } rescue Gem::InvalidSpecificationException => e error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \ "The validation error was '#{e.message}'\n" @@ -57,11 +61,6 @@ module Bundler nil end - def set_installed_by_version(spec, installed_by_version = Gem::VERSION) - return unless spec.respond_to?(:installed_by_version=) - spec.installed_by_version = Gem::Version.create(installed_by_version) - end - def spec_missing_extensions?(spec, default = true) return spec.missing_extensions? if spec.respond_to?(:missing_extensions?) @@ -216,7 +215,7 @@ module Bundler [::Kernel.singleton_class, ::Kernel].each do |kernel_class| redefine_method(kernel_class, :gem) do |dep, *reqs| - if executables&.include?(File.basename(caller.first.split(":").first)) + if executables&.include?(File.basename(caller_locations(1, 1).first.path)) break end @@ -481,11 +480,25 @@ module Bundler end def all_specs + SharedHelpers.major_deprecation 2, "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs" + Gem::Specification.stubs.map do |stub| StubSpecification.from_stub(stub) end end + def installed_specs + Gem::Specification.stubs.reject(&:default_gem?).map do |stub| + StubSpecification.from_stub(stub) + end + end + + def default_specs + Gem::Specification.default_stubs.map do |stub| + StubSpecification.from_stub(stub) + end + end + def find_bundler(version) find_name("bundler").find {|s| s.version.to_s == version } end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index 54aa30ce0b..9792a81962 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -10,7 +10,7 @@ module Bundler end def setup(*groups) - @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen_bundle? + @definition.ensure_equivalent_gemfile_and_lockfile # Has to happen first clean_load_path @@ -139,11 +139,6 @@ module Bundler spec.source.cache(spec, custom_path) if spec.source.respond_to?(:cache) end - Dir[cache_path.join("*/.git")].each do |git_dir| - FileUtils.rm_rf(git_dir) - FileUtils.touch(File.expand_path("../.bundlecache", git_dir)) - end - prune_cache(cache_path) unless Bundler.settings[:no_prune] end diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb index bfd000b1a0..a6d93f20ff 100644 --- a/lib/bundler/self_manager.rb +++ b/lib/bundler/self_manager.rb @@ -70,8 +70,23 @@ module Bundler configured_gem_home = ENV["GEM_HOME"] configured_gem_path = ENV["GEM_PATH"] - cmd = [$PROGRAM_NAME, *ARGV] - cmd.unshift(Gem.ruby) unless File.executable?($PROGRAM_NAME) + # Bundler specs need some stuff to be required before Bundler starts + # running, for example, for faking the compact index API. However, these + # flags are lost when we reexec to a different version of Bundler. In the + # future, we may be able to properly reconstruct the original Ruby + # invocation (see https://bugs.ruby-lang.org/issues/6648), but for now + # there's no way to do it, so we need to be explicit about how to re-exec. + # This may be a feature end users request at some point, but maybe by that + # time, we have builtin tools to do. So for now, we use an undocumented + # ENV variable only for our specs. + bundler_spec_original_cmd = ENV["BUNDLER_SPEC_ORIGINAL_CMD"] + if bundler_spec_original_cmd + require "shellwords" + cmd = [*Shellwords.shellsplit(bundler_spec_original_cmd), *ARGV] + else + cmd = [$PROGRAM_NAME, *ARGV] + cmd.unshift(Gem.ruby) unless File.executable?($PROGRAM_NAME) + end Bundler.with_original_env do Kernel.exec( @@ -83,15 +98,16 @@ module Bundler def needs_switching? autoswitching_applies? && - released?(lockfile_version) && - !running?(lockfile_version) && - !updating? && - Bundler.settings[:version] != "system" + Bundler.settings[:version] != "system" && + released?(restart_version) && + !running?(restart_version) && + !updating? end def autoswitching_applies? ENV["BUNDLER_VERSION"].nil? && Bundler.rubygems.supports_bundler_trampolining? && + ruby_can_restart_with_same_arguments? && SharedHelpers.in_bundle? && lockfile_version end @@ -113,7 +129,7 @@ module Bundler end def local_specs - @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true, "allow_cached" => true).specs.select {|spec| spec.name == "bundler" } + @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true).specs.select {|spec| spec.name == "bundler" } end def remote_specs @@ -151,6 +167,10 @@ module Bundler !version.to_s.end_with?(".dev") end + def ruby_can_restart_with_same_arguments? + $PROGRAM_NAME != "-e" + end + def updating? "update".start_with?(ARGV.first || " ") && ARGV[1..-1].any? {|a| a.start_with?("--bundler") } end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index abbaec9783..878747a53b 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -103,6 +103,7 @@ module Bundler def initialize(root = nil) @root = root @local_config = load_config(local_config_file) + @local_root = root || Pathname.new(".bundle").expand_path @env_config = ENV.to_h @env_config.select! {|key, _value| key.start_with?("BUNDLE_") } @@ -142,7 +143,7 @@ module Bundler end def set_local(key, value) - local_config_file || raise(GemfileNotFound, "Could not locate Gemfile") + local_config_file = @local_root.join("config") set_key(key, value, @local_config, local_config_file) end @@ -491,6 +492,10 @@ module Bundler valid_file = file.exist? && !file.size.zero? return {} unless valid_file serializer_class.load(file.read).inject({}) do |config, (k, v)| + k = k.dup + k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}") + k.gsub!(".", "__") + unless k.start_with?("#") if k.include?("-") Bundler.ui.warn "Your #{file} config includes `#{k}`, which contains the dash character (`-`).\n" \ @@ -518,26 +523,25 @@ module Bundler YAMLSerializer end - PER_URI_OPTIONS = %w[ - fallback_timeout - ].freeze + FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout" NORMALIZE_URI_OPTIONS_PATTERN = / \A (\w+\.)? # optional prefix key (https?.*?) # URI - (\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key + (\.#{FALLBACK_TIMEOUT_URI_OPTION})? # optional suffix key \z /ix def self.key_for(key) - key = normalize_uri(key).to_s if key.is_a?(String) && key.start_with?("http", "mirror.http") - key = key_to_s(key).gsub(".", "__") + key = key_to_s(key) + key = normalize_uri(key) if key.start_with?("http", "mirror.http") + key = key.gsub(".", "__") key.gsub!("-", "___") key.upcase! - key.prepend("BUNDLE_") + key.gsub(/\A([ #]*)/, '\1BUNDLE_') end # TODO: duplicates Rubygems#normalize_uri diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb index 6010d66742..5a0fd8e0e3 100644 --- a/lib/bundler/setup.rb +++ b/lib/bundler/setup.rb @@ -5,6 +5,9 @@ require_relative "shared_helpers" if Bundler::SharedHelpers.in_bundle? require_relative "../bundler" + # autoswitch to locked Bundler version if available + Bundler.auto_switch + # try to auto_install first before we get to the `Bundler.ui.silence`, so user knows what is happening Bundler.auto_install diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 78760e6fa4..e55632b89f 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true -require "pathname" -require "rbconfig" - require_relative "version" -require_relative "constants" require_relative "rubygems_integration" require_relative "current_ruby" +autoload :Pathname, "pathname" + module Bundler + autoload :WINDOWS, File.expand_path("constants", __dir__) + autoload :FREEBSD, File.expand_path("constants", __dir__) + autoload :NULL, File.expand_path("constants", __dir__) + module SharedHelpers def root gemfile = find_gemfile diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index 198e335bb6..78f9ff0560 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -32,6 +32,20 @@ module Bundler @local = false end + def remote! + return if @allow_remote + + @local_specs = nil + @allow_remote = true + end + + def cached! + return if @allow_cached + + @local_specs = nil + @allow_cached = true + end + def self.from_lock(options) new(options.merge("uri" => options.delete("remote"))) end @@ -56,13 +70,13 @@ module Bundler end def hash - [self.class, uri, ref, branch, name, version, glob, submodules].hash + [self.class, uri, ref, branch, name, glob, submodules].hash end def eql?(other) other.is_a?(Git) && uri == other.uri && ref == other.ref && branch == other.branch && name == other.name && - version == other.version && glob == other.glob && + glob == other.glob && submodules == other.submodules end @@ -150,7 +164,8 @@ module Bundler "does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override" end - set_local!(path) + @local = true + set_paths!(path) # Create a new git proxy without the cached revision # so the Gemfile.lock always picks up the new revision. @@ -173,13 +188,13 @@ module Bundler end def specs(*) - set_local!(app_cache_path) if has_app_cache? && !local? + set_up_app_cache!(app_cache_path) if use_app_cache? if requires_checkout? && !@copied + FileUtils.rm_rf(app_cache_path) if use_app_cache? && git_proxy.not_a_bare_repository? + fetch - git_proxy.copy_to(install_path, submodules) - serialize_gemspecs_in(install_path) - @copied = true + checkout end local_specs @@ -192,10 +207,7 @@ module Bundler print_using_message "Using #{version_message(spec, options[:previous_spec])} from #{self}" if (requires_checkout? && !@copied) || force - Bundler.ui.debug " * Checking out revision: #{ref}" - git_proxy.copy_to(install_path, submodules) - serialize_gemspecs_in(install_path) - @copied = true + checkout end generate_bin_options = { disable_extensions: !Bundler.rubygems.spec_missing_extensions?(spec), build_args: options[:build_args] } @@ -207,12 +219,14 @@ module Bundler def cache(spec, custom_path = nil) app_cache_path = app_cache_path(custom_path) return unless Bundler.feature_flag.cache_all? - return if path == app_cache_path + return if install_path == app_cache_path + return if cache_path == app_cache_path cached! FileUtils.rm_rf(app_cache_path) git_proxy.checkout if requires_checkout? - git_proxy.copy_to(app_cache_path, @submodules) - serialize_gemspecs_in(app_cache_path) + FileUtils.cp_r("#{cache_path}/.", app_cache_path) + FileUtils.touch(app_cache_path.join(".bundlecache")) + FileUtils.rm_rf(Dir.glob(app_cache_path.join("hooks/*.sample"))) end def load_spec_files @@ -256,6 +270,13 @@ module Bundler private + def checkout + Bundler.ui.debug " * Checking out revision: #{ref}" + git_proxy.copy_to(install_path, submodules) + serialize_gemspecs_in(install_path) + @copied = true + end + def humanized_ref if local? path @@ -278,22 +299,40 @@ module Bundler # The gemspecs we cache should already be evaluated. spec = Bundler.load_gemspec(spec_path) next unless spec - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION Bundler.rubygems.validate(spec) File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) } end end - def set_local!(path) - @local = true - @local_specs = @git_proxy = nil - @cache_path = @install_path = path + def set_paths!(path) + set_cache_path!(path) + set_install_path!(path) + end + + def set_cache_path!(path) + @git_proxy = nil + @cache_path = path + end + + def set_install_path!(path) + @local_specs = nil + @install_path = path + end + + def set_up_app_cache!(path) + FileUtils.mkdir_p(path.join("refs")) + set_cache_path!(path) end def has_app_cache? cached_revision && super end + def use_app_cache? + has_app_cache? && !local? + end + def requires_checkout? allow_git_ops? && !local? && !cached_revision_checked_out? end @@ -359,9 +398,12 @@ module Bundler def validate_spec(_spec); end def load_gemspec(file) - stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) - stub.full_gem_path = Pathname.new(file).dirname.expand_path(root).to_s - StubSpecification.from_stub(stub) + dirname = Pathname.new(file).dirname + SharedHelpers.chdir(dirname.to_s) do + stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent) + stub.full_gem_path = dirname.expand_path(root).to_s + StubSpecification.from_stub(stub) + end end def git_scope diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 645851286c..768d40392f 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -84,6 +84,10 @@ module Bundler end end + def not_a_bare_repository? + git_local("rev-parse", "--is-bare-repository", dir: path).strip == "false" + end + def contains?(commit) allowed_with_path do result, status = git_null("branch", "--contains", commit, dir: path) @@ -182,6 +186,14 @@ module Bundler if err.include?("Could not find remote branch") raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri) else + idx = command.index("--depth") + if idx + command.delete_at(idx) + command.delete_at(idx) + command_with_no_credentials = check_allowed(command) + + err += "Retrying without --depth argument." + end raise GitCommandError.new(command_with_no_credentials, path, err) end end @@ -324,8 +336,6 @@ module Bundler config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] remote.userinfo ||= config_auth remote.to_s - elsif File.exist?(uri) - "file://#{uri}" else uri.to_s end diff --git a/lib/bundler/source/metadata.rb b/lib/bundler/source/metadata.rb index 4d27761365..6b05e17727 100644 --- a/lib/bundler/source/metadata.rb +++ b/lib/bundler/source/metadata.rb @@ -11,6 +11,8 @@ module Bundler end if local_spec = Gem.loaded_specs["bundler"] + raise CorruptBundlerInstallError.new(local_spec) if local_spec.version.to_s != Bundler::VERSION + idx << local_spec else idx << Gem::Specification.new do |s| diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 978b0b2c9f..6a99f8e977 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -18,9 +18,6 @@ module Bundler @options = options.dup @glob = options["glob"] || DEFAULT_GLOB - @allow_cached = false - @allow_remote = false - @root_path = options["root_path"] || root if options["path"] @@ -41,16 +38,6 @@ module Bundler @original_path = @path end - def remote! - @local_specs = nil - @allow_remote = true - end - - def cached! - @local_specs = nil - @allow_cached = true - end - def self.from_lock(options) new(options.merge("path" => options.delete("remote"))) end @@ -161,7 +148,7 @@ module Bundler def load_gemspec(file) return unless spec = Bundler.load_gemspec(file) - Bundler.rubygems.set_installed_by_version(spec) + spec.installed_by_version = Gem::VERSION spec end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 2e76becb84..3b6ef8bd58 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -17,11 +17,13 @@ module Bundler @remotes = [] @dependency_names = [] @allow_remote = false - @allow_cached = options["allow_cached"] || false + @allow_cached = false @allow_local = options["allow_local"] || false @checksum_store = Checksum::Store.new Array(options["remotes"]).reverse_each {|r| add_remote(r) } + + @lockfile_remotes = @remotes if options["from_lockfile"] end def caches @@ -50,10 +52,11 @@ module Bundler end def cached! + return unless File.exist?(cache_path) + return if @allow_cached @specs = nil - @allow_local = true @allow_cached = true end @@ -90,13 +93,13 @@ module Bundler def self.from_lock(options) options["remotes"] = Array(options.delete("remote")).reverse - new(options) + new(options.merge("from_lockfile" => true)) end def to_lock out = String.new("GEM\n") - remotes.reverse_each do |remote| - out << " remote: #{remove_auth remote}\n" + lockfile_remotes.reverse_each do |remote| + out << " remote: #{remote}\n" end out << " specs:\n" end @@ -135,20 +138,17 @@ module Bundler index = @allow_remote ? remote_specs.dup : Index.new index.merge!(cached_specs) if @allow_cached index.merge!(installed_specs) if @allow_local + + # complete with default specs, only if not already available in the + # index through remote, cached, or installed specs + index.use(default_specs) if @allow_local + index end end def install(spec, options = {}) - force = options[:force] - ensure_builtin_gems_cached = options[:ensure_builtin_gems_cached] - - if ensure_builtin_gems_cached && spec.default_gem? && !cached_path(spec) - cached_built_in_gem(spec) unless spec.remote - force = true - end - - if installed?(spec) && !force + if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force]) print_using_message "Using #{version_message(spec, options[:previous_spec])}" return nil # no post-install message end @@ -206,6 +206,7 @@ module Bundler spec.full_gem_path = installed_spec.full_gem_path spec.loaded_from = installed_spec.loaded_from + spec.base_dir = installed_spec.base_dir spec.post_install_message end @@ -221,12 +222,13 @@ module Bundler raise InstallError, e.message end - def cached_built_in_gem(spec) - cached_path = cached_path(spec) - if cached_path.nil? + def cached_built_in_gem(spec, local: false) + cached_path = cached_gem(spec) + if cached_path.nil? && !local remote_spec = remote_specs.search(spec).first if remote_spec cached_path = fetch_gem(remote_spec) + spec.remote = remote_spec.remote else Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it." end @@ -323,14 +325,6 @@ module Bundler end def cached_gem(spec) - if spec.default_gem? - cached_built_in_gem(spec) - else - cached_path(spec) - end - end - - def cached_path(spec) global_cache_path = download_cache_path(spec) caches << global_cache_path if global_cache_path @@ -361,7 +355,7 @@ module Bundler def installed_specs @installed_specs ||= Index.build do |idx| - Bundler.rubygems.all_specs.reverse_each do |spec| + Bundler.rubygems.installed_specs.reverse_each do |spec| spec.source = self if Bundler.rubygems.spec_missing_extensions?(spec, false) Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions" @@ -372,6 +366,15 @@ module Bundler end end + def default_specs + @default_specs ||= Index.build do |idx| + Bundler.rubygems.default_specs.each do |spec| + spec.source = self + idx << spec + end + end + end + def cached_specs @cached_specs ||= begin idx = Index.new @@ -456,6 +459,10 @@ module Bundler private + def lockfile_remotes + @lockfile_remotes || credless_remotes + end + # Checks if the requested spec exists in the global cache. If it does, # we copy it to the download path, and if it does not, we download it. # diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index bbaac33a95..5f9dd68f17 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -9,7 +9,7 @@ module Bundler :metadata_source def global_rubygems_source - @global_rubygems_source ||= rubygems_aggregate_class.new("allow_local" => true, "allow_cached" => true) + @global_rubygems_source ||= rubygems_aggregate_class.new("allow_local" => true) end def initialize @@ -22,6 +22,7 @@ module Bundler @metadata_source = Source::Metadata.new @merged_gem_lockfile_sections = false + @local_mode = true end def merged_gem_lockfile_sections? @@ -73,6 +74,10 @@ module Bundler global_rubygems_source end + def local_mode? + @local_mode + end + def default_source global_path_source || global_rubygems_source end @@ -140,11 +145,17 @@ module Bundler all_sources.each(&:local_only!) end + def local! + all_sources.each(&:local!) + end + def cached! all_sources.each(&:cached!) end def remote! + @local_mode = false + all_sources.each(&:remote!) end @@ -178,7 +189,7 @@ module Bundler replacement_source = replace_rubygems_source(replacement_sources, global_rubygems_source) return global_rubygems_source unless replacement_source - replacement_source.cached! + replacement_source.local! replacement_source end diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 2933d28450..96c36c2dec 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -71,12 +71,6 @@ module Bundler platforms end - def complete_platforms!(platforms) - platforms.each do |platform| - complete_platform(platform) - end - end - def validate_deps(s) s.runtime_dependencies.each do |dep| next if dep.name == "bundler" @@ -100,7 +94,7 @@ module Bundler end def delete(specs) - specs.each {|spec| @specs.delete(spec) } + Array(specs).each {|spec| @specs.delete(spec) } reset! end @@ -158,6 +152,12 @@ module Bundler @specs.detect {|spec| spec.name == name && spec.match_platform(platform) } end + def specs_compatible_with(other) + select do |spec| + other.valid?(spec) + end + end + def delete_by_name(name) @specs.reject! {|spec| spec.name == name } @@ -195,6 +195,10 @@ module Bundler lookup.keys end + def valid?(s) + s.matches_current_metadata? && valid_dependencies?(s) + end + private def reset! @@ -209,7 +213,7 @@ module Bundler spec = specs.first matching_specs = spec.source.specs.search([spec.name, spec.version]) platform_spec = GemHelpers.select_best_platform_match(matching_specs, platform).find do |s| - s.matches_current_metadata? && valid_dependencies?(s) + valid?(s) end if platform_spec @@ -273,13 +277,11 @@ module Bundler specs_for_name = lookup[dep.name] return [] unless specs_for_name - matching_specs = if dep.force_ruby_platform - GemHelpers.force_ruby_platform(specs_for_name) + if platform + GemHelpers.select_best_platform_match(specs_for_name, platform, force_ruby: dep.force_ruby_platform) else - GemHelpers.select_best_platform_match(specs_for_name, platform || Bundler.local_platform) + GemHelpers.select_best_local_platform_match(specs_for_name, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform) end - matching_specs.map!(&:materialize_for_installation).compact! if platform.nil? - matching_specs end def tsort_each_child(s) diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb index da830cf8d4..1cbb506ef9 100644 --- a/lib/bundler/stub_specification.rb +++ b/lib/bundler/stub_specification.rb @@ -77,6 +77,14 @@ module Bundler stub.full_require_paths end + def require_paths + stub.require_paths + end + + def base_dir=(path) + stub.base_dir = path + end + def load_paths full_require_paths end diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt index 175b821a62..67fe8cee79 100644 --- a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt +++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt @@ -2,83 +2,131 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to a positive environment for our community include: +Examples of behavior that contributes to a positive environment for our +community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or - advances of any kind +* The use of sexualized language or imagery, and sexual attention or advances of + any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <%= config[:email] %>. All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the reporter of any incident. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. -**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. ### 2. Warning -**Community Impact**: A violation through a single incident or series of actions. +**Community Impact**: A violation through a single incident or series of +actions. -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. ### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within the community. +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt index 5bf36378e8..f9c97d5c7e 100644 --- a/lib/bundler/templates/newgem/README.md.tt +++ b/lib/bundler/templates/newgem/README.md.tt @@ -10,11 +10,15 @@ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_O Install the gem and add to the application's Gemfile by executing: - $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +```bash +bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` If bundler is not being used to manage dependencies, install the gem by executing: - $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +```bash +gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` ## Usage diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb index 4555612dbb..6df1512a5b 100644 --- a/lib/bundler/ui/shell.rb +++ b/lib/bundler/ui/shell.rb @@ -6,14 +6,17 @@ module Bundler module UI class Shell LEVELS = %w[silent error warn confirm info debug].freeze + OUTPUT_STREAMS = [:stdout, :stderr].freeze attr_writer :shell + attr_reader :output_stream def initialize(options = {}) Thor::Base.shell = options["no-color"] ? Thor::Shell::Basic : nil @shell = Thor::Base.shell.new @level = ENV["DEBUG"] ? "debug" : "info" @warning_history = [] + @output_stream = :stdout end def add_color(string, *color) @@ -84,7 +87,7 @@ module Bundler @shell.yes?(msg) end - def no? + def no?(msg) @shell.no?(msg) end @@ -101,6 +104,11 @@ module Bundler index <= LEVELS.index(@level) end + def output_stream=(symbol) + raise ArgumentError unless OUTPUT_STREAMS.include?(symbol) + @output_stream = symbol + end + def trace(e, newline = nil, force = false) return unless debug? || force msg = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n ")}" @@ -111,6 +119,10 @@ module Bundler with_level("silent", &blk) end + def progress(&blk) + with_output_stream(:stderr, &blk) + end + def unprinted_warnings [] end @@ -119,6 +131,8 @@ module Bundler # valimism def tell_me(msg, color = nil, newline = nil) + return tell_err(msg, color, newline) if output_stream == :stderr + msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap] if newline.nil? @shell.say(msg, color) @@ -130,7 +144,7 @@ module Bundler def tell_err(message, color = nil, newline = nil) return if @shell.send(:stderr).closed? - newline ||= !message.to_s.match?(/( |\t)\Z/) + newline = !message.to_s.match?(/( |\t)\Z/) if newline.nil? message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap] color = nil if color && !$stderr.tty? @@ -160,6 +174,14 @@ module Bundler ensure @level = original end + + def with_output_stream(symbol) + original = output_stream + self.output_stream = symbol + yield + ensure + @output_stream = original + end end end end diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb index fa3292bdc9..83d31d4b55 100644 --- a/lib/bundler/ui/silent.rb +++ b/lib/bundler/ui/silent.rb @@ -53,6 +53,13 @@ module Bundler false end + def output_stream=(_symbol) + end + + def output_stream + nil + end + def ask(message) end @@ -60,7 +67,7 @@ module Bundler raise "Cannot ask yes? with a silent shell" end - def no? + def no?(msg) raise "Cannot ask no? with a silent shell" end @@ -77,6 +84,10 @@ module Bundler yield end + def progress + yield + end + def unprinted_warnings @warnings end diff --git a/lib/bundler/vendor/securerandom/.document b/lib/bundler/vendor/securerandom/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/bundler/vendor/securerandom/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/bundler/vendor/securerandom/lib/random/formatter.rb b/lib/bundler/vendor/securerandom/lib/random/formatter.rb new file mode 100644 index 0000000000..e429709789 --- /dev/null +++ b/lib/bundler/vendor/securerandom/lib/random/formatter.rb @@ -0,0 +1,373 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +# == \Random number formatter. +# +# Formats generated random numbers in many manners. When <tt>'random/formatter'</tt> +# is required, several methods are added to empty core module <tt>Bundler::Random::Formatter</tt>, +# making them available as Random's instance and module methods. +# +# Standard library Bundler::SecureRandom is also extended with the module, and the methods +# described below are available as a module methods in it. +# +# === Examples +# +# Generate random hexadecimal strings: +# +# require 'bundler/vendor/securerandom/lib/random/formatter' +# +# prng = Random.new +# prng.hex(10) #=> "52750b30ffbc7de3b362" +# prng.hex(10) #=> "92b15d6c8dc4beb5f559" +# prng.hex(13) #=> "39b290146bea6ce975c37cfc23" +# # or just +# Random.hex #=> "1aed0c631e41be7f77365415541052ee" +# +# Generate random base64 strings: +# +# prng.base64(10) #=> "EcmTPZwWRAozdA==" +# prng.base64(10) #=> "KO1nIU+p9DKxGg==" +# prng.base64(12) #=> "7kJSM/MzBJI+75j8" +# Random.base64(4) #=> "bsQ3fQ==" +# +# Generate random binary strings: +# +# prng.random_bytes(10) #=> "\016\t{\370g\310pbr\301" +# prng.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337" +# Random.random_bytes(6) #=> "\xA1\xE6Lr\xC43" +# +# Generate alphanumeric strings: +# +# prng.alphanumeric(10) #=> "S8baxMJnPl" +# prng.alphanumeric(10) #=> "aOxAg8BAJe" +# Random.alphanumeric #=> "TmP9OsJHJLtaZYhP" +# +# Generate UUIDs: +# +# prng.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594" +# prng.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab" +# Random.uuid #=> "f14e0271-de96-45cc-8911-8910292a42cd" +# +# All methods are available in the standard library Bundler::SecureRandom, too: +# +# Bundler::SecureRandom.hex #=> "05b45376a30c67238eb93b16499e50cf" + +module Bundler::Random::Formatter + + # Generate a random binary string. + # + # The argument _n_ specifies the length of the result string. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in future. + # + # The result may contain any byte: "\x00" - "\xff". + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6" + # # or + # prng = Random.new + # prng.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97" + def random_bytes(n=nil) + n = n ? n.to_int : 16 + gen_random(n) + end + + # Generate a random hexadecimal string. + # + # The argument _n_ specifies the length, in bytes, of the random number to be generated. + # The length of the resulting hexadecimal string is twice of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain 0-9 and a-f. + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.hex #=> "eb693ec8252cd630102fd0d0fb7c3485" + # # or + # prng = Random.new + # prng.hex #=> "91dc3bfb4de5b11d029d376634589b61" + def hex(n=nil) + random_bytes(n).unpack1("H*") + end + + # Generate a random base64 string. + # + # The argument _n_ specifies the length, in bytes, of the random number + # to be generated. The length of the result string is about 4/3 of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain A-Z, a-z, 0-9, "+", "/" and "=". + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A==" + # # or + # prng = Random.new + # prng.base64 #=> "6BbW0pxO0YENxn38HMUbcQ==" + # + # See RFC 3548 for the definition of base64. + def base64(n=nil) + [random_bytes(n)].pack("m0") + end + + # Generate a random URL-safe base64 string. + # + # The argument _n_ specifies the length, in bytes, of the random number + # to be generated. The length of the result string is about 4/3 of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The boolean argument _padding_ specifies the padding. + # If it is false or nil, padding is not generated. + # Otherwise padding is generated. + # By default, padding is not generated because "=" may be used as a URL delimiter. + # + # The result may contain A-Z, a-z, 0-9, "-" and "_". + # "=" is also used if _padding_ is true. + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg" + # # or + # prng = Random.new + # prng.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg" + # + # prng.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ==" + # prng.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg==" + # + # See RFC 3548 for the definition of URL-safe base64. + def urlsafe_base64(n=nil, padding=false) + s = [random_bytes(n)].pack("m0") + s.tr!("+/", "-_") + s.delete!("=") unless padding + s + end + + # Generate a random v4 UUID (Universally Unique IDentifier). + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594" + # Random.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab" + # # or + # prng = Random.new + # prng.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b" + # + # The version 4 UUID is purely random (except the version). + # It doesn't contain meaningful information such as MAC addresses, timestamps, etc. + # + # The result contains 122 random bits (15.25 random bytes). + # + # See RFC4122[https://datatracker.ietf.org/doc/html/rfc4122] for details of UUID. + # + def uuid + ary = random_bytes(16).unpack("NnnnnN") + ary[2] = (ary[2] & 0x0fff) | 0x4000 + ary[3] = (ary[3] & 0x3fff) | 0x8000 + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + alias uuid_v4 uuid + + # Generate a random v7 UUID (Universally Unique IDentifier). + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.uuid_v7 # => "0188d4c3-1311-7f96-85c7-242a7aa58f1e" + # Random.uuid_v7 # => "0188d4c3-16fe-744f-86af-38fa04c62bb5" + # Random.uuid_v7 # => "0188d4c3-1af8-764f-b049-c204ce0afa23" + # Random.uuid_v7 # => "0188d4c3-1e74-7085-b14f-ef6415dc6f31" + # # |<--sorted-->| |<----- random ---->| + # + # # or + # prng = Random.new + # prng.uuid_v7 # => "0188ca51-5e72-7950-a11d-def7ff977c98" + # + # The version 7 UUID starts with the least significant 48 bits of a 64 bit + # Unix timestamp (milliseconds since the epoch) and fills the remaining bits + # with random data, excluding the version and variant bits. + # + # This allows version 7 UUIDs to be sorted by creation time. Time ordered + # UUIDs can be used for better database index locality of newly inserted + # records, which may have a significant performance benefit compared to random + # data inserts. + # + # The result contains 74 random bits (9.25 random bytes). + # + # Note that this method cannot be made reproducable because its output + # includes not only random bits but also timestamp. + # + # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/] + # for details of UUIDv7. + # + # ==== Monotonicity + # + # UUIDv7 has millisecond precision by default, so multiple UUIDs created + # within the same millisecond are not issued in monotonically increasing + # order. To create UUIDs that are time-ordered with sub-millisecond + # precision, up to 12 bits of additional timestamp may added with + # +extra_timestamp_bits+. The extra timestamp precision comes at the expense + # of random bits. Setting <tt>extra_timestamp_bits: 12</tt> provides ~244ns + # of precision, but only 62 random bits (7.75 random bytes). + # + # prng = Random.new + # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 12) } + # # => + # ["0188d4c7-13da-74f9-8b53-22a786ffdd5a", + # "0188d4c7-13da-753b-83a5-7fb9b2afaeea", + # "0188d4c7-13da-754a-88ea-ac0baeedd8db", + # "0188d4c7-13da-7557-83e1-7cad9cda0d8d"] + # # |<--- sorted --->| |<-- random --->| + # + # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 8) } + # # => + # ["0188d4c7-3333-7a95-850a-de6edb858f7e", + # "0188d4c7-3333-7ae8-842e-bc3a8b7d0cf9", # <- out of order + # "0188d4c7-3333-7ae2-995a-9f135dc44ead", # <- out of order + # "0188d4c7-3333-7af9-87c3-8f612edac82e"] + # # |<--- sorted -->||<---- random --->| + # + # Any rollbacks of the system clock will break monotonicity. UUIDv7 is based + # on UTC, which excludes leap seconds and can rollback the clock. To avoid + # this, the system clock can synchronize with an NTP server configured to use + # a "leap smear" approach. NTP or PTP will also be needed to synchronize + # across distributed nodes. + # + # Counters and other mechanisms for stronger guarantees of monotonicity are + # not implemented. Applications with stricter requirements should follow + # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters] + # of the specification. + # + def uuid_v7(extra_timestamp_bits: 0) + case (extra_timestamp_bits = Integer(extra_timestamp_bits)) + when 0 # min timestamp precision + ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) + rand = random_bytes(10) + rand.setbyte(0, rand.getbyte(0) & 0x0f | 0x70) # version + rand.setbyte(2, rand.getbyte(2) & 0x3f | 0x80) # variant + "%08x-%04x-%s" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + rand.unpack("H4H4H12").join("-") + ] + + when 12 # max timestamp precision + ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .divmod(1_000_000) + extra_bits = ns * 4096 / 1_000_000 + rand = random_bytes(8) + rand.setbyte(0, rand.getbyte(0) & 0x3f | 0x80) # variant + "%08x-%04x-7%03x-%s" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + extra_bits, + rand.unpack("H4H12").join("-") + ] + + when (0..12) # the generic version is slower than the special cases above + rand_a, rand_b1, rand_b2, rand_b3 = random_bytes(10).unpack("nnnN") + rand_mask_bits = 12 - extra_timestamp_bits + ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .divmod(1_000_000) + "%08x-%04x-%04x-%04x-%04x%08x" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + 0x7000 | + ((ns * (1 << extra_timestamp_bits) / 1_000_000) << rand_mask_bits) | + rand_a & ((1 << rand_mask_bits) - 1), + 0x8000 | (rand_b1 & 0x3fff), + rand_b2, + rand_b3 + ] + + else + raise ArgumentError, "extra_timestamp_bits must be in 0..12" + end + end + + # Internal interface to Random; Generate random data _n_ bytes. + private def gen_random(n) + self.bytes(n) + end + + # Generate a string that randomly draws from a + # source array of characters. + # + # The argument _source_ specifies the array of characters from which + # to generate the string. + # The argument _n_ specifies the length, in characters, of the string to be + # generated. + # + # The result may contain whatever characters are in the source array. + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # prng.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron" + # prng.choose([*'0'..'9'], 5) #=> "27309" + private def choose(source, n) + size = source.size + m = 1 + limit = size + while limit * size <= 0x100000000 + limit *= size + m += 1 + end + result = ''.dup + while m <= n + rs = random_number(limit) + is = rs.digits(size) + (m-is.length).times { is << 0 } + result << source.values_at(*is).join('') + n -= m + end + if 0 < n + rs = random_number(limit) + is = rs.digits(size) + if is.length < n + (n-is.length).times { is << 0 } + else + is.pop while n < is.length + end + result.concat source.values_at(*is).join('') + end + result + end + + # The default character list for #alphanumeric. + ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9'] + + # Generate a random alphanumeric string. + # + # The argument _n_ specifies the length, in characters, of the alphanumeric + # string to be generated. + # The argument _chars_ specifies the character list which the result is + # consist of. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain A-Z, a-z and 0-9, unless _chars_ is specified. + # + # require 'bundler/vendor/securerandom/lib/random/formatter' + # + # Random.alphanumeric #=> "2BuBuLf3WfSKyQbR" + # # or + # prng = Random.new + # prng.alphanumeric(10) #=> "i6K93NdqiH" + # + # Random.alphanumeric(4, chars: [*"0".."9"]) #=> "2952" + # # or + # prng = Random.new + # prng.alphanumeric(10, chars: [*"!".."/"]) #=> ",.,++%/''." + def alphanumeric(n = nil, chars: ALPHANUMERIC) + n = 16 if n.nil? + choose(chars, n) + end +end diff --git a/lib/bundler/vendor/securerandom/lib/securerandom.rb b/lib/bundler/vendor/securerandom/lib/securerandom.rb new file mode 100644 index 0000000000..e797054468 --- /dev/null +++ b/lib/bundler/vendor/securerandom/lib/securerandom.rb @@ -0,0 +1,96 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +require_relative 'random/formatter' + +# == Secure random number generator interface. +# +# This library is an interface to secure random number generators which are +# suitable for generating session keys in HTTP cookies, etc. +# +# You can use this library in your application by requiring it: +# +# require 'bundler/vendor/securerandom/lib/securerandom' +# +# It supports the following secure random number generators: +# +# * openssl +# * /dev/urandom +# * Win32 +# +# Bundler::SecureRandom is extended by the Bundler::Random::Formatter module which +# defines the following methods: +# +# * alphanumeric +# * base64 +# * choose +# * gen_random +# * hex +# * rand +# * random_bytes +# * random_number +# * urlsafe_base64 +# * uuid +# +# These methods are usable as class methods of Bundler::SecureRandom such as +# +Bundler::SecureRandom.hex+. +# +# If a secure random number generator is not available, +# +NotImplementedError+ is raised. + +module Bundler::SecureRandom + + # The version + VERSION = "0.3.1" + + class << self + # Returns a random binary string containing +size+ bytes. + # + # See Random.bytes + def bytes(n) + return gen_random(n) + end + + private + + # :stopdoc: + + # Implementation using OpenSSL + def gen_random_openssl(n) + return OpenSSL::Random.random_bytes(n) + end + + # Implementation using system random device + def gen_random_urandom(n) + ret = Random.urandom(n) + unless ret + raise NotImplementedError, "No random device" + end + unless ret.length == n + raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes" + end + ret + end + + begin + # Check if Random.urandom is available + Random.urandom(1) + alias gen_random gen_random_urandom + rescue RuntimeError + begin + require 'openssl' + rescue NoMethodError + raise NotImplementedError, "No random device" + else + alias gen_random gen_random_openssl + end + end + + # :startdoc: + + # Generate random data bytes for Bundler::Random::Formatter + public :gen_random + end +end + +Bundler::SecureRandom.extend(Bundler::Random::Formatter) diff --git a/lib/bundler/vendored_net_http.rb b/lib/bundler/vendored_net_http.rb index 0dcabaa7d7..8ff2ccd1fe 100644 --- a/lib/bundler/vendored_net_http.rb +++ b/lib/bundler/vendored_net_http.rb @@ -1,12 +1,23 @@ # frozen_string_literal: true -begin - require "rubygems/vendored_net_http" -rescue LoadError +# This defined? guard can be removed once RubyGems 3.4 support is dropped. +# +# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid +# activating the Bundler gem too early. Without this guard, we get redefinition +# warnings once Bundler is actually activated and +# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in +# RubyGems versions including `rubygems/vendored_net_http` since `require` takes +# care of avoiding the double load. +# +unless defined?(Gem::Net) begin - require "rubygems/net/http" + require "rubygems/vendored_net_http" rescue LoadError - require "net/http" - Gem::Net = Net + begin + require "rubygems/net/http" + rescue LoadError + require "net/http" + Gem::Net = Net + end end end diff --git a/lib/bundler/vendored_securerandom.rb b/lib/bundler/vendored_securerandom.rb new file mode 100644 index 0000000000..6c15f4a2b2 --- /dev/null +++ b/lib/bundler/vendored_securerandom.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Use RubyGems vendored copy when available. Otherwise fallback to Bundler +# vendored copy. The vendored copy in Bundler can be removed once support for +# RubyGems 3.5.18 is dropped. + +begin + require "rubygems/vendored_securerandom" +rescue LoadError + module Bundler::Random; end + require_relative "vendor/securerandom/lib/securerandom" + Gem::SecureRandom = Bundler::SecureRandom + Gem::Random = Bundler::Random +end diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb index 42e6aaf89d..ab1eb6dbcf 100644 --- a/lib/bundler/yaml_serializer.rb +++ b/lib/bundler/yaml_serializer.rb @@ -41,7 +41,7 @@ module Bundler HASH_REGEX = / ^ ([ ]*) # indentations - (.+) # key + ([^#]+) # key excludes comment char '#' (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) [ ]? (['"]?) # optional opening quote @@ -60,7 +60,6 @@ module Bundler indent, key, quote, val = match.captures val = strip_comment(val) - convert_to_backward_compatible_key!(key) depth = indent.size / 2 if quote.empty? && val.empty? new_hash = {} @@ -92,14 +91,8 @@ module Bundler end end - # for settings' keys - def convert_to_backward_compatible_key!(key) - key << "/" if /https?:/i.match?(key) && !%r{/\Z}.match?(key) - key.gsub!(".", "__") - end - class << self - private :dump_hash, :convert_to_backward_compatible_key! + private :dump_hash end end end diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb index b9c68b8eb8..e2077fa5a6 100644 --- a/lib/error_highlight/base.rb +++ b/lib/error_highlight/base.rb @@ -60,14 +60,14 @@ module ErrorHighlight rescue RuntimeError => error # RubyVM::AbstractSyntaxTree.of raises an error with a message that # includes "prism" when the ISEQ was compiled with the prism compiler. - # In this case, we'll set the node to `nil`. In the future, we will - # reparse with the prism parser and pass the parsed node to Spotter. + # In this case, we'll try to parse again with prism instead. raise unless error.message.include?("prism") + prism_find(loc) end Spotter.new(node, **opts).spot - when RubyVM::AbstractSyntaxTree::Node + when RubyVM::AbstractSyntaxTree::Node, Prism::Node Spotter.new(obj, **opts).spot else @@ -81,6 +81,21 @@ module ErrorHighlight return nil end + # Accepts a Thread::Backtrace::Location object and returns a Prism::Node + # corresponding to the backtrace location in the source code. + def self.prism_find(location) + require "prism" + return nil if Prism::VERSION < "1.0.0" + + absolute_path = location.absolute_path + return unless absolute_path + + node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location) + Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id } + end + + private_class_method :prism_find + class Spotter class NonAscii < Exception; end private_constant :NonAscii @@ -113,31 +128,49 @@ module ErrorHighlight def spot return nil unless @node - if OPT_GETCONSTANT_PATH && @node.type == :COLON2 + if OPT_GETCONSTANT_PATH # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`) # is compiled to one instruction (opt_getconstant_path). # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo` # or `Foo::Bar` causes NameError. # So we try to spot the sub-node that causes the NameError by using # `NameError#name`. - subnodes = [] - node = @node - while node.type == :COLON2 - node2, const = node.children - subnodes << node if const == @name - node = node2 - end - if node.type == :CONST || node.type == :COLON3 - if node.children.first == @name + case @node.type + when :COLON2 + subnodes = [] + node = @node + while node.type == :COLON2 + node2, const = node.children + subnodes << node if const == @name + node = node2 + end + if node.type == :CONST || node.type == :COLON3 + if node.children.first == @name + subnodes << node + end + + # If we found only one sub-node whose name is equal to @name, use it + return nil if subnodes.size != 1 + @node = subnodes.first + else + # Do nothing; opt_getconstant_path is used only when the const base is + # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`) + end + when :constant_path_node + subnodes = [] + node = @node + + begin + subnodes << node if node.name == @name + end while (node = node.parent).is_a?(Prism::ConstantPathNode) + + if node.is_a?(Prism::ConstantReadNode) && node.name == @name subnodes << node end # If we found only one sub-node whose name is equal to @name, use it return nil if subnodes.size != 1 @node = subnodes.first - else - # Do nothing; opt_getconstant_path is used only when the const base is - # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`) end end @@ -205,6 +238,48 @@ module ErrorHighlight when :OP_CDECL spot_op_cdecl + + when :call_node + case @point_type + when :name + prism_spot_call_for_name + when :args + prism_spot_call_for_args + end + + when :local_variable_operator_write_node + case @point_type + when :name + prism_spot_local_variable_operator_write_for_name + when :args + prism_spot_local_variable_operator_write_for_args + end + + when :call_operator_write_node + case @point_type + when :name + prism_spot_call_operator_write_for_name + when :args + prism_spot_call_operator_write_for_args + end + + when :index_operator_write_node + case @point_type + when :name + prism_spot_index_operator_write_for_name + when :args + prism_spot_index_operator_write_for_args + end + + when :constant_read_node + prism_spot_constant_read + + when :constant_path_node + prism_spot_constant_path + + when :constant_path_operator_write_node + prism_spot_constant_path_operator_write + end if @snippet && @beg_column && @end_column && @beg_column < @end_column @@ -548,6 +623,207 @@ module ErrorHighlight @beg_lineno = @end_lineno = lineno @snippet = @fetch[lineno] end + + # Take a location from the prism parser and set the necessary instance + # variables. + def prism_location(location) + @beg_lineno = location.start_line + @beg_column = location.start_column + @end_lineno = location.end_line + @end_column = location.end_column + @snippet = @fetch[@beg_lineno, @end_lineno] + end + + # Example: + # x.foo + # ^^^^ + # x.foo(42) + # ^^^^ + # x&.foo + # ^^^^^ + # x[42] + # ^^^^ + # x.foo = 1 + # ^^^^^^ + # x[42] = 1 + # ^^^^^^ + # x + 1 + # ^ + # +x + # ^ + # foo(42) + # ^^^ + # foo 42 + # ^^^ + # foo + # ^^^ + def prism_spot_call_for_name + # Explicitly turn off foo.() syntax because error_highlight expects this + # to not work. + return nil if @node.name == :call && @node.message_loc.nil? + + location = @node.message_loc || @node.call_operator_loc || @node.location + location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line + + # If the method name ends with "=" but the message does not, then this is + # a method call using the "attribute assignment" syntax + # (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and + # add it to the location. + if (name = @node.name).end_with?("=") && !@node.message.end_with?("=") + location = location.adjoin("=") + end + + prism_location(location) + + if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/) + # If the method name is an operator, then error_highlight only + # highlights the first line. + fetch_line(location.start_line) + end + end + + # Example: + # x.foo(42) + # ^^ + # x[42] + # ^^ + # x.foo = 1 + # ^ + # x[42] = 1 + # ^^^^^^^ + # x[] = 1 + # ^^^^^ + # x + 1 + # ^ + # foo(42) + # ^^ + # foo 42 + # ^^ + def prism_spot_call_for_args + # Disallow highlighting arguments if there are no arguments. + return if @node.arguments.nil? + + # Explicitly turn off foo.() syntax because error_highlight expects this + # to not work. + return nil if @node.name == :call && @node.message_loc.nil? + + if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1 + prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location)) + else + prism_location(@node.arguments.location) + end + end + + # Example: + # x += 1 + # ^ + def prism_spot_local_variable_operator_write_for_name + prism_location(@node.binary_operator_loc.chop) + end + + # Example: + # x += 1 + # ^ + def prism_spot_local_variable_operator_write_for_args + prism_location(@node.value.location) + end + + # Example: + # x.foo += 42 + # ^^^ (for foo) + # x.foo += 42 + # ^ (for +) + # x.foo += 42 + # ^^^^^^^ (for foo=) + def prism_spot_call_operator_write_for_name + if !@name.start_with?(/[[:alpha:]_]/) + prism_location(@node.binary_operator_loc.chop) + else + location = @node.message_loc + if @node.call_operator_loc.start_line == location.start_line + location = @node.call_operator_loc.join(location) + end + + location = location.adjoin("=") if @name.end_with?("=") + prism_location(location) + end + end + + # Example: + # x.foo += 42 + # ^^ + def prism_spot_call_operator_write_for_args + prism_location(@node.value.location) + end + + # Example: + # x[1] += 42 + # ^^^ (for []) + # x[1] += 42 + # ^ (for +) + # x[1] += 42 + # ^^^^^^ (for []=) + def prism_spot_index_operator_write_for_name + case @name + when :[] + prism_location(@node.opening_loc.join(@node.closing_loc)) + when :[]= + prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("=")) + else + # Explicitly turn off foo[] += 1 syntax when the operator is not on + # the same line because error_highlight expects this to not work. + return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line + + prism_location(@node.binary_operator_loc.chop) + end + end + + # Example: + # x[1] += 42 + # ^^^^^^^^ + def prism_spot_index_operator_write_for_args + opening_loc = + if @node.arguments.nil? + @node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1) + else + @node.arguments.location + end + + prism_location(opening_loc.join(@node.value.location)) + end + + # Example: + # Foo + # ^^^ + def prism_spot_constant_read + prism_location(@node.location) + end + + # Example: + # Foo::Bar + # ^^^^^ + def prism_spot_constant_path + if @node.parent && @node.parent.location.end_line == @node.location.end_line + fetch_line(@node.parent.location.end_line) + prism_location(@node.delimiter_loc.join(@node.name_loc)) + else + fetch_line(@node.location.end_line) + location = @node.name_loc + location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line + prism_location(location) + end + end + + # Example: + # Foo::Bar += 1 + # ^^^^^^^^ + def prism_spot_constant_path_operator_write + if @name == (target = @node.target).name + prism_location(target.delimiter_loc.join(target.name_loc)) + else + prism_location(@node.binary_operator_loc.chop) + end + end end private_constant :Spotter diff --git a/lib/fileutils.gemspec b/lib/fileutils.gemspec index 76baea3039..2603d664da 100644 --- a/lib/fileutils.gemspec +++ b/lib/fileutils.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.description = "Several file utility methods for copying, moving, removing, etc." s.require_path = %w{lib} - s.files = ["LICENSE.txt", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"] + s.files = ["COPYING", "BSDL", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"] s.required_ruby_version = ">= 2.5.0" s.authors = ["Minero Aoki"] diff --git a/lib/find.gemspec b/lib/find.gemspec index cb845e9409..aef24a5028 100644 --- a/lib/find.gemspec +++ b/lib/find.gemspec @@ -25,7 +25,5 @@ Gem::Specification.new do |spec| spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] end diff --git a/lib/irb.rb b/lib/irb.rb index b3435c257e..213e231174 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -880,40 +880,42 @@ module IRB # An exception raised by IRB.irb_abort class Abort < Exception;end - # The current IRB::Context of the session, see IRB.conf - # - # irb - # irb(main):001:0> IRB.CurrentContext.irb_name = "foo" - # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo" - def IRB.CurrentContext # :nodoc: - IRB.conf[:MAIN_CONTEXT] - end + class << self + # The current IRB::Context of the session, see IRB.conf + # + # irb + # irb(main):001:0> IRB.CurrentContext.irb_name = "foo" + # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo" + def CurrentContext # :nodoc: + conf[:MAIN_CONTEXT] + end - # Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING` - def IRB.start(ap_path = nil) - STDOUT.sync = true - $0 = File::basename(ap_path, ".rb") if ap_path + # Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING` + def start(ap_path = nil) + STDOUT.sync = true + $0 = File::basename(ap_path, ".rb") if ap_path - IRB.setup(ap_path) + setup(ap_path) - if @CONF[:SCRIPT] - irb = Irb.new(nil, @CONF[:SCRIPT]) - else - irb = Irb.new + if @CONF[:SCRIPT] + irb = Irb.new(nil, @CONF[:SCRIPT]) + else + irb = Irb.new + end + irb.run(@CONF) end - irb.run(@CONF) - end - # Quits irb - def IRB.irb_exit(*) # :nodoc: - throw :IRB_EXIT, false - end + # Quits irb + def irb_exit(*) # :nodoc: + throw :IRB_EXIT, false + end - # Aborts then interrupts irb. - # - # Will raise an Abort exception, or the given `exception`. - def IRB.irb_abort(irb, exception = Abort) # :nodoc: - irb.context.thread.raise exception, "abort then interrupt!" + # Aborts then interrupts irb. + # + # Will raise an Abort exception, or the given `exception`. + def irb_abort(irb, exception = Abort) # :nodoc: + irb.context.thread.raise exception, "abort then interrupt!" + end end class Irb @@ -1129,7 +1131,7 @@ module IRB end code.force_encoding(@context.io.encoding) - if (command, arg = parse_command(code)) + if (command, arg = @context.parse_command(code)) command_class = Command.load_command(command) Statement::Command.new(code, command_class, arg) else @@ -1138,27 +1140,8 @@ module IRB end end - def parse_command(code) - command_name, arg = code.strip.split(/\s+/, 2) - return unless code.lines.size == 1 && command_name - - arg ||= '' - command = command_name.to_sym - # Command aliases are always command. example: $, @ - if (alias_name = @context.command_aliases[command]) - return [alias_name, arg] - end - - # Check visibility - public_method = !!Kernel.instance_method(:public_method).bind_call(@context.main, command) rescue false - private_method = !public_method && !!Kernel.instance_method(:method).bind_call(@context.main, command) rescue false - if Command.execute_as_command?(command, public_method: public_method, private_method: private_method) - [command, arg] - end - end - def command?(code) - !!parse_command(code) + !!@context.parse_command(code) end def configure_io diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb index 1d406630a2..af810ed343 100644 --- a/lib/irb/command/base.rb +++ b/lib/irb/command/base.rb @@ -10,8 +10,10 @@ module IRB module Command class CommandArgumentError < StandardError; end - def self.extract_ruby_args(*args, **kwargs) - throw :EXTRACT_RUBY_ARGS, [args, kwargs] + class << self + def extract_ruby_args(*args, **kwargs) + throw :EXTRACT_RUBY_ARGS, [args, kwargs] + end end class Base @@ -31,6 +33,12 @@ module IRB @help_message end + def execute(irb_context, arg) + new(irb_context).execute(arg) + rescue CommandArgumentError => e + puts e.message + end + private def highlight(text) @@ -38,12 +46,6 @@ module IRB end end - def self.execute(irb_context, arg) - new(irb_context).execute(arg) - rescue CommandArgumentError => e - puts e.message - end - def initialize(irb_context) @irb_context = irb_context end diff --git a/lib/irb/command/cd.rb b/lib/irb/command/cd.rb new file mode 100644 index 0000000000..b83c8689ae --- /dev/null +++ b/lib/irb/command/cd.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module IRB + module Command + class CD < Base + category "Workspace" + description "Move into the given object or leave the current context." + + help_message(<<~HELP) + Usage: cd ([target]|..) + + IRB uses a stack of workspaces to keep track of context(s), with `pushws` and `popws` commands to manipulate the stack. + The `cd` command is an attempt to simplify the operation and will be subject to change. + + When given: + - an object, cd will use that object as the new context by pushing it onto the workspace stack. + - "..", cd will leave the current context by popping the top workspace off the stack. + - no arguments, cd will move to the top workspace on the stack by popping off all workspaces. + + Examples: + + cd Foo + cd Foo.new + cd @ivar + cd .. + cd + HELP + + def execute(arg) + case arg + when ".." + irb_context.pop_workspace + when "" + # TODO: decide what workspace commands should be kept, and underlying APIs should look like, + # and perhaps add a new API to clear the workspace stack. + prev_workspace = irb_context.pop_workspace + while prev_workspace + prev_workspace = irb_context.pop_workspace + end + else + begin + obj = eval(arg, irb_context.workspace.binding) + irb_context.push_workspace(obj) + rescue StandardError => e + warn "Error: #{e}" + end + end + end + end + end +end diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb index 8a091a49ed..3ebb57fe54 100644 --- a/lib/irb/command/debug.rb +++ b/lib/irb/command/debug.rb @@ -58,13 +58,15 @@ module IRB end class DebugCommand < Debug - def self.category - "Debugging" - end + class << self + def category + "Debugging" + end - def self.description - command_name = self.name.split("::").last.downcase - "Start the debugger of debug.gem and run its `#{command_name}` command." + def description + command_name = self.name.split("::").last.downcase + "Start the debugger of debug.gem and run its `#{command_name}` command." + end end end end diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb index c2018f9b30..12b468fefc 100644 --- a/lib/irb/command/help.rb +++ b/lib/irb/command/help.rb @@ -39,7 +39,7 @@ module IRB help_cmds = commands_grouped_by_categories.delete("Help") no_category_cmds = commands_grouped_by_categories.delete("No category") - aliases = irb_context.instance_variable_get(:@user_aliases).map do |alias_name, target| + aliases = irb_context.instance_variable_get(:@command_aliases).map do |alias_name, target| { display_name: alias_name, description: "Alias for `#{target}`" } end diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index a3d89373c3..7f102dcdf4 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -33,6 +33,8 @@ module IRB yield ] + HELP_COMMAND_PREPOSING = /\Ahelp\s+/ + def completion_candidates(preposing, target, postposing, bind:) raise NotImplementedError end @@ -86,8 +88,8 @@ module IRB ) end - def command_completions(preposing, target) - if preposing.empty? && !target.empty? + def command_candidates(target) + if !target.empty? IRB::Command.command_names.select { _1.start_with?(target) } else [] @@ -111,8 +113,18 @@ module IRB end def completion_candidates(preposing, target, _postposing, bind:) - commands = command_completions(preposing, target) + # When completing the argument of `help` command, only commands should be candidates + return command_candidates(target) if preposing.match?(HELP_COMMAND_PREPOSING) + + commands = if preposing.empty? + command_candidates(target) + # It doesn't make sense to propose commands with other preposing + else + [] + end + result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path) + return commands unless result commands | result.completion_candidates.map { target + _1 } @@ -187,12 +199,20 @@ module IRB end def completion_candidates(preposing, target, postposing, bind:) - if preposing && postposing - result = complete_require_path(target, preposing, postposing) - return result if result + if result = complete_require_path(target, preposing, postposing) + return result end - commands = command_completions(preposing || '', target) - commands | retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) } + + commands = command_candidates(target) + + # When completing the argument of `help` command, only commands should be candidates + return commands if preposing.match?(HELP_COMMAND_PREPOSING) + + # It doesn't make sense to propose commands with other preposing + commands = [] unless preposing.empty? + + completion_data = retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) } + commands | completion_data end def doc_namespace(_preposing, matched, _postposing, bind:) @@ -470,7 +490,7 @@ module IRB end end CompletionProc = ->(target, preposing = nil, postposing = nil) { - regexp_completor.completion_candidates(preposing, target, postposing, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) + regexp_completor.completion_candidates(preposing || '', target, postposing || '', bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) } end deprecate_constant :InputCompletor diff --git a/lib/irb/context.rb b/lib/irb/context.rb index aafce7aade..668a823f5c 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -13,6 +13,7 @@ module IRB # A class that wraps the current state of the irb session, including the # configuration of IRB.conf. class Context + ASSIGN_OPERATORS_REGEXP = Regexp.union(%w[= += -= *= /= %= **= &= |= &&= ||= ^= <<= >>=]) # Creates a new IRB context. # # The optional +input_method+ argument: @@ -148,8 +149,7 @@ module IRB @newline_before_multiline_output = true end - @user_aliases = IRB.conf[:COMMAND_ALIASES].dup - @command_aliases = @user_aliases.merge(KEYWORD_ALIASES) + @command_aliases = IRB.conf[:COMMAND_ALIASES].dup end private def term_interactive? @@ -157,17 +157,6 @@ module IRB STDIN.tty? && ENV['TERM'] != 'dumb' end - # because all input will eventually be evaluated as Ruby code, - # command names that conflict with Ruby keywords need special workaround - # we can remove them once we implemented a better command system for IRB - KEYWORD_ALIASES = { - :break => :irb_break, - :catch => :irb_catch, - :next => :irb_next, - }.freeze - - private_constant :KEYWORD_ALIASES - def use_tracer=(val) require_relative "ext/tracer" if val IRB.conf[:USE_TRACER] = val @@ -602,7 +591,6 @@ module IRB set_last_value(result) when Statement::Command statement.command_class.execute(self, statement.arg) - set_last_value(nil) end nil @@ -636,6 +624,46 @@ module IRB result end + def parse_command(code) + command_name, arg = code.strip.split(/\s+/, 2) + return unless code.lines.size == 1 && command_name + + arg ||= '' + command = command_name.to_sym + # Command aliases are always command. example: $, @ + if (alias_name = command_aliases[command]) + return [alias_name, arg] + end + + # Assignment-like expression is not a command + return if arg.start_with?(ASSIGN_OPERATORS_REGEXP) && !arg.start_with?(/==|=~/) + + # Local variable have precedence over command + return if local_variables.include?(command) + + # Check visibility + public_method = !!Kernel.instance_method(:public_method).bind_call(main, command) rescue false + private_method = !public_method && !!Kernel.instance_method(:method).bind_call(main, command) rescue false + if Command.execute_as_command?(command, public_method: public_method, private_method: private_method) + [command, arg] + end + end + + def colorize_input(input, complete:) + if IRB.conf[:USE_COLORIZE] && IRB::Color.colorable? + lvars = local_variables || [] + if parse_command(input) + name, sep, arg = input.split(/(\s+)/, 2) + arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars) + "#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}" + else + IRB::Color.colorize_code(input, complete: complete, local_variables: lvars) + end + else + Reline::Unicode.escape_for_print(input) + end + end + def inspect_last_value # :nodoc: @inspect_method.inspect_value(@last_value) end diff --git a/lib/irb/debug.rb b/lib/irb/debug.rb index 1ec2335a8e..cd64b77ad7 100644 --- a/lib/irb/debug.rb +++ b/lib/irb/debug.rb @@ -57,22 +57,18 @@ module IRB DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB) end - if !@output_modifier_defined && !DEBUGGER__::CONFIG[:no_hint] - irb_output_modifier_proc = Reline.output_modifier_proc - - Reline.output_modifier_proc = proc do |output, complete:| - unless output.strip.empty? - cmd = output.split(/\s/, 2).first + if !DEBUGGER__::CONFIG[:no_hint] && irb.context.io.is_a?(RelineInputMethod) + Reline.output_modifier_proc = proc do |input, complete:| + unless input.strip.empty? + cmd = input.split(/\s/, 2).first if !complete && DEBUGGER__.commands.key?(cmd) - output = output.sub(/\n$/, " # debug command\n") + input = input.sub(/\n$/, " # debug command\n") end end - irb_output_modifier_proc.call(output, complete: complete) + irb.context.colorize_input(input, complete: complete) end - - @output_modifier_defined = true end true diff --git a/lib/irb/default_commands.rb b/lib/irb/default_commands.rb index 1bbc68efa7..768fbee9d7 100644 --- a/lib/irb/default_commands.rb +++ b/lib/irb/default_commands.rb @@ -2,32 +2,34 @@ require_relative "command" require_relative "command/internal_helpers" -require_relative "command/context" -require_relative "command/exit" -require_relative "command/force_exit" -require_relative "command/chws" -require_relative "command/pushws" -require_relative "command/subirb" -require_relative "command/load" -require_relative "command/debug" -require_relative "command/edit" +require_relative "command/backtrace" require_relative "command/break" require_relative "command/catch" -require_relative "command/next" -require_relative "command/delete" -require_relative "command/step" +require_relative "command/cd" +require_relative "command/chws" +require_relative "command/context" require_relative "command/continue" +require_relative "command/debug" +require_relative "command/delete" +require_relative "command/disable_irb" +require_relative "command/edit" +require_relative "command/exit" require_relative "command/finish" -require_relative "command/backtrace" -require_relative "command/info" +require_relative "command/force_exit" require_relative "command/help" -require_relative "command/show_doc" +require_relative "command/history" +require_relative "command/info" require_relative "command/irb_info" +require_relative "command/load" require_relative "command/ls" require_relative "command/measure" +require_relative "command/next" +require_relative "command/pushws" +require_relative "command/show_doc" require_relative "command/show_source" +require_relative "command/step" +require_relative "command/subirb" require_relative "command/whereami" -require_relative "command/history" module IRB module Command @@ -179,9 +181,15 @@ module IRB [:edit, NO_OVERRIDE] ) - _register_with_aliases(:irb_break, Command::Break) - _register_with_aliases(:irb_catch, Command::Catch) - _register_with_aliases(:irb_next, Command::Next) + _register_with_aliases(:irb_break, Command::Break, + [:break, OVERRIDE_ALL] + ) + _register_with_aliases(:irb_catch, Command::Catch, + [:catch, OVERRIDE_PRIVATE_ONLY] + ) + _register_with_aliases(:irb_next, Command::Next, + [:next, OVERRIDE_ALL] + ) _register_with_aliases(:irb_delete, Command::Delete, [:delete, NO_OVERRIDE] ) @@ -235,6 +243,12 @@ module IRB [:history, NO_OVERRIDE], [:hist, NO_OVERRIDE] ) + + _register_with_aliases(:irb_disable_irb, Command::DisableIrb, + [:disable_irb, NO_OVERRIDE] + ) + + register(:cd, Command::CD) end ExtendCommand = Command @@ -251,10 +265,12 @@ module IRB # Deprecated. Doesn't have any effect. @EXTEND_COMMANDS = [] - # Drepcated. Use Command.regiser instead. - def self.def_extend_command(cmd_name, cmd_class, _, *aliases) - Command._register_with_aliases(cmd_name, cmd_class, *aliases) - Command.class_variable_set(:@@command_override_policies, nil) + class << self + # Drepcated. Use Command.regiser instead. + def def_extend_command(cmd_name, cmd_class, _, *aliases) + Command._register_with_aliases(cmd_name, cmd_class, *aliases) + Command.class_variable_set(:@@command_override_policies, nil) + end end end end diff --git a/lib/irb/easter-egg.rb b/lib/irb/easter-egg.rb index adc0834d55..14dc93fc9c 100644 --- a/lib/irb/easter-egg.rb +++ b/lib/irb/easter-egg.rb @@ -100,19 +100,21 @@ module IRB private def easter_egg_logo(type) @easter_egg_logos ||= File.read(File.join(__dir__, 'ruby_logo.aa'), encoding: 'UTF-8:UTF-8') - .split(/TYPE: ([A-Z]+)\n/)[1..] + .split(/TYPE: ([A-Z_]+)\n/)[1..] .each_slice(2) .to_h @easter_egg_logos[type.to_s.upcase] end private def easter_egg(type = nil) + print "\e[?1049h" type ||= [:logo, :dancing].sample case type when :logo - require "rdoc" - RDoc::RI::Driver.new.page do |io| - io.write easter_egg_logo(:large) + Pager.page do |io| + logo_type = STDOUT.external_encoding == Encoding::UTF_8 ? :unicode_large : :ascii_large + io.write easter_egg_logo(logo_type) + STDIN.raw { STDIN.getc } if io == STDOUT end when :dancing STDOUT.cooked do @@ -137,10 +139,11 @@ module IRB end rescue Interrupt ensure - print "\e[0m\e[?1049l" trap("SIGINT", prev_trap) end end + ensure + print "\e[0m\e[?1049l" end end end diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index 684527edc4..210d3da789 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -171,11 +171,13 @@ module IRB end class ReadlineInputMethod < StdioInputMethod - def self.initialize_readline - require "readline" - rescue LoadError - else - include ::Readline + class << self + def initialize_readline + require "readline" + rescue LoadError + else + include ::Readline + end end include HistorySavingAbility @@ -263,18 +265,9 @@ module IRB @completion_params = [preposing, target, postposing, bind] @completor.completion_candidates(preposing, target, postposing, bind: bind) } - Reline.output_modifier_proc = - if IRB.conf[:USE_COLORIZE] - proc do |output, complete: | - next unless IRB::Color.colorable? - lvars = IRB.CurrentContext&.local_variables || [] - IRB::Color.colorize_code(output, complete: complete, local_variables: lvars) - end - else - proc do |output| - Reline::Unicode.escape_for_print(output) - end - end + Reline.output_modifier_proc = proc do |input, complete:| + IRB.CurrentContext.colorize_input(input, complete: complete) + end Reline.dig_perfect_match_proc = ->(matched) { display_document(matched) } Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE] @@ -328,10 +321,11 @@ module IRB ->() { dialog.trap_key = nil alt_d = [ - [Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d. [27, 100], # Normal Alt+d when convert-meta isn't used. - [195, 164], # The "ä" that appears when Alt+d is pressed on xterm. - [226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2. + # When option/alt is not configured as a meta key in terminal emulator, + # option/alt + d will send a unicode character depend on OS keyboard setting. + [195, 164], # "ä" in somewhere (FIXME: environment information is unknown). + [226, 136, 130] # "∂" Alt+d on Mac keyboard. ] if just_cursor_moving and completion_journey_data.nil? diff --git a/lib/irb/inspector.rb b/lib/irb/inspector.rb index 667087ccba..8046744f88 100644 --- a/lib/irb/inspector.rb +++ b/lib/irb/inspector.rb @@ -6,7 +6,6 @@ module IRB # :nodoc: - # Convenience method to create a new Inspector, using the given +inspect+ # proc, and optional +init+ proc and passes them to Inspector.new # @@ -43,38 +42,40 @@ module IRB # :nodoc: # +:marshal+:: Using Marshal.dump INSPECTORS = {} - # Determines the inspector to use where +inspector+ is one of the keys passed - # during inspector definition. - def self.keys_with_inspector(inspector) - INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k} - end - - # Example - # - # Inspector.def_inspector(key, init_p=nil){|v| v.inspect} - # Inspector.def_inspector([key1,..], init_p=nil){|v| v.inspect} - # Inspector.def_inspector(key, inspector) - # Inspector.def_inspector([key1,...], inspector) - def self.def_inspector(key, arg=nil, &block) - if block_given? - inspector = IRB::Inspector(block, arg) - else - inspector = arg + class << self + # Determines the inspector to use where +inspector+ is one of the keys passed + # during inspector definition. + def keys_with_inspector(inspector) + INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k} end - case key - when Array - for k in key - def_inspector(k, inspector) + # Example + # + # Inspector.def_inspector(key, init_p=nil){|v| v.inspect} + # Inspector.def_inspector([key1,..], init_p=nil){|v| v.inspect} + # Inspector.def_inspector(key, inspector) + # Inspector.def_inspector([key1,...], inspector) + def def_inspector(key, arg=nil, &block) + if block_given? + inspector = IRB::Inspector(block, arg) + else + inspector = arg + end + + case key + when Array + for k in key + def_inspector(k, inspector) + end + when Symbol + INSPECTORS[key] = inspector + INSPECTORS[key.to_s] = inspector + when String + INSPECTORS[key] = inspector + INSPECTORS[key.intern] = inspector + else + INSPECTORS[key] = inspector end - when Symbol - INSPECTORS[key] = inspector - INSPECTORS[key.to_s] = inspector - when String - INSPECTORS[key] = inspector - INSPECTORS[key.intern] = inspector - else - INSPECTORS[key] = inspector end end diff --git a/lib/irb/nesting_parser.rb b/lib/irb/nesting_parser.rb index 5aa940cc28..fc71d64aee 100644 --- a/lib/irb/nesting_parser.rb +++ b/lib/irb/nesting_parser.rb @@ -3,235 +3,237 @@ module IRB module NestingParser IGNORE_TOKENS = %i[on_sp on_ignored_nl on_comment on_embdoc_beg on_embdoc on_embdoc_end] - # Scan each token and call the given block with array of token and other information for parsing - def self.scan_opens(tokens) - opens = [] - pending_heredocs = [] - first_token_on_line = true - tokens.each do |t| - skip = false - last_tok, state, args = opens.last - case state - when :in_alias_undef - skip = t.event == :on_kw - when :in_unquoted_symbol - unless IGNORE_TOKENS.include?(t.event) - opens.pop - skip = true - end - when :in_lambda_head - opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do') - when :in_method_head - unless IGNORE_TOKENS.include?(t.event) - next_args = [] - body = nil - if args.include?(:receiver) - case t.event - when :on_lparen, :on_ivar, :on_gvar, :on_cvar - # def (receiver). | def @ivar. | def $gvar. | def @@cvar. - next_args << :dot - when :on_kw - case t.tok - when 'self', 'true', 'false', 'nil' - # def self(arg) | def self. - next_args.push(:arg, :dot) - else - # def if(arg) + class << self + # Scan each token and call the given block with array of token and other information for parsing + def scan_opens(tokens) + opens = [] + pending_heredocs = [] + first_token_on_line = true + tokens.each do |t| + skip = false + last_tok, state, args = opens.last + case state + when :in_alias_undef + skip = t.event == :on_kw + when :in_unquoted_symbol + unless IGNORE_TOKENS.include?(t.event) + opens.pop + skip = true + end + when :in_lambda_head + opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do') + when :in_method_head + unless IGNORE_TOKENS.include?(t.event) + next_args = [] + body = nil + if args.include?(:receiver) + case t.event + when :on_lparen, :on_ivar, :on_gvar, :on_cvar + # def (receiver). | def @ivar. | def $gvar. | def @@cvar. + next_args << :dot + when :on_kw + case t.tok + when 'self', 'true', 'false', 'nil' + # def self(arg) | def self. + next_args.push(:arg, :dot) + else + # def if(arg) + skip = true + next_args << :arg + end + when :on_op, :on_backtick + # def +(arg) skip = true next_args << :arg + when :on_ident, :on_const + # def a(arg) | def a. + next_args.push(:arg, :dot) end - when :on_op, :on_backtick - # def +(arg) - skip = true - next_args << :arg - when :on_ident, :on_const - # def a(arg) | def a. - next_args.push(:arg, :dot) end - end - if args.include?(:dot) - # def receiver.name - next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::') - end - if args.include?(:name) - if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event) - # def name(arg) | def receiver.name(arg) - next_args << :arg - skip = true + if args.include?(:dot) + # def receiver.name + next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::') end - end - if args.include?(:arg) - case t.event - when :on_nl, :on_semicolon - # def receiver.f; - body = :normal - when :on_lparen - # def receiver.f() - next_args << :eq - else + if args.include?(:name) + if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event) + # def name(arg) | def receiver.name(arg) + next_args << :arg + skip = true + end + end + if args.include?(:arg) + case t.event + when :on_nl, :on_semicolon + # def receiver.f; + body = :normal + when :on_lparen + # def receiver.f() + next_args << :eq + else + if t.event == :on_op && t.tok == '=' + # def receiver.f = + body = :oneliner + else + # def receiver.f arg + next_args << :arg_without_paren + end + end + end + if args.include?(:eq) if t.event == :on_op && t.tok == '=' - # def receiver.f = body = :oneliner else - # def receiver.f arg - next_args << :arg_without_paren + body = :normal end end - end - if args.include?(:eq) - if t.event == :on_op && t.tok == '=' - body = :oneliner - else - body = :normal + if args.include?(:arg_without_paren) + if %i[on_semicolon on_nl].include?(t.event) + # def f a; + body = :normal + else + # def f a, b + next_args << :arg_without_paren + end end - end - if args.include?(:arg_without_paren) - if %i[on_semicolon on_nl].include?(t.event) - # def f a; - body = :normal + if body == :oneliner + opens.pop + elsif body + opens[-1] = [last_tok, nil] else - # def f a, b - next_args << :arg_without_paren + opens[-1] = [last_tok, :in_method_head, next_args] end end - if body == :oneliner - opens.pop - elsif body + when :in_for_while_until_condition + if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do') + skip = true if t.event == :on_kw && t.tok == 'do' opens[-1] = [last_tok, nil] - else - opens[-1] = [last_tok, :in_method_head, next_args] end end - when :in_for_while_until_condition - if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do') - skip = true if t.event == :on_kw && t.tok == 'do' - opens[-1] = [last_tok, nil] - end - end - unless skip - case t.event - when :on_kw - case t.tok - when 'begin', 'class', 'module', 'do', 'case' - opens << [t, nil] - when 'end' - opens.pop - when 'def' - opens << [t, :in_method_head, [:receiver, :name]] - when 'if', 'unless' - unless t.state.allbits?(Ripper::EXPR_LABEL) + unless skip + case t.event + when :on_kw + case t.tok + when 'begin', 'class', 'module', 'do', 'case' opens << [t, nil] - end - when 'while', 'until' - unless t.state.allbits?(Ripper::EXPR_LABEL) - opens << [t, :in_for_while_until_condition] - end - when 'ensure', 'rescue' - unless t.state.allbits?(Ripper::EXPR_LABEL) + when 'end' + opens.pop + when 'def' + opens << [t, :in_method_head, [:receiver, :name]] + when 'if', 'unless' + unless t.state.allbits?(Ripper::EXPR_LABEL) + opens << [t, nil] + end + when 'while', 'until' + unless t.state.allbits?(Ripper::EXPR_LABEL) + opens << [t, :in_for_while_until_condition] + end + when 'ensure', 'rescue' + unless t.state.allbits?(Ripper::EXPR_LABEL) + opens.pop + opens << [t, nil] + end + when 'alias' + opens << [t, :in_alias_undef, 2] + when 'undef' + opens << [t, :in_alias_undef, 1] + when 'elsif', 'else', 'when' opens.pop opens << [t, nil] + when 'for' + opens << [t, :in_for_while_until_condition] + when 'in' + if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line + opens.pop + opens << [t, nil] + end end - when 'alias' - opens << [t, :in_alias_undef, 2] - when 'undef' - opens << [t, :in_alias_undef, 1] - when 'elsif', 'else', 'when' + when :on_tlambda + opens << [t, :in_lambda_head] + when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg + opens << [t, nil] + when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end + opens.pop + when :on_heredoc_beg + pending_heredocs << t + when :on_heredoc_end opens.pop + when :on_backtick + opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG) + when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg opens << [t, nil] - when 'for' - opens << [t, :in_for_while_until_condition] - when 'in' - if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line - opens.pop + when :on_tstring_end, :on_regexp_end, :on_label_end + opens.pop + when :on_symbeg + if t.tok == ':' + opens << [t, :in_unquoted_symbol] + else opens << [t, nil] end end - when :on_tlambda - opens << [t, :in_lambda_head] - when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg - opens << [t, nil] - when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end - opens.pop - when :on_heredoc_beg - pending_heredocs << t - when :on_heredoc_end - opens.pop - when :on_backtick - opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG) - when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg - opens << [t, nil] - when :on_tstring_end, :on_regexp_end, :on_label_end - opens.pop - when :on_symbeg - if t.tok == ':' - opens << [t, :in_unquoted_symbol] - else - opens << [t, nil] - end end + if t.event == :on_nl || t.event == :on_semicolon + first_token_on_line = true + elsif t.event != :on_sp + first_token_on_line = false + end + if pending_heredocs.any? && t.tok.include?("\n") + pending_heredocs.reverse_each { |t| opens << [t, nil] } + pending_heredocs = [] + end + if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end + tok, state, arg = opens.pop + opens << [tok, state, arg - 1] if arg >= 1 + end + yield t, opens if block_given? end - if t.event == :on_nl || t.event == :on_semicolon - first_token_on_line = true - elsif t.event != :on_sp - first_token_on_line = false - end - if pending_heredocs.any? && t.tok.include?("\n") - pending_heredocs.reverse_each { |t| opens << [t, nil] } - pending_heredocs = [] - end - if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end - tok, state, arg = opens.pop - opens << [tok, state, arg - 1] if arg >= 1 - end - yield t, opens if block_given? + opens.map(&:first) + pending_heredocs.reverse end - opens.map(&:first) + pending_heredocs.reverse - end - def self.open_tokens(tokens) - # scan_opens without block will return a list of open tokens at last token position - scan_opens(tokens) - end + def open_tokens(tokens) + # scan_opens without block will return a list of open tokens at last token position + scan_opens(tokens) + end - # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line. - # Example code - # ["hello - # world"+( - # First line - # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]] - # prev_opens: [] - # next_tokens: [lbracket, tstring_beg] - # min_depth: 0 (minimum at beginning of line) - # Second line - # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']] - # prev_opens: [lbracket, tstring_beg] - # next_tokens: [lbracket, lparen] - # min_depth: 1 (minimum just after tstring_end) - def self.parse_by_line(tokens) - line_tokens = [] - prev_opens = [] - min_depth = 0 - output = [] - last_opens = scan_opens(tokens) do |t, opens| - depth = t == opens.last&.first ? opens.size - 1 : opens.size - min_depth = depth if depth < min_depth - if t.tok.include?("\n") - t.tok.each_line do |line| - line_tokens << [t, line] - next if line[-1] != "\n" - next_opens = opens.map(&:first) - output << [line_tokens, prev_opens, next_opens, min_depth] - prev_opens = next_opens - min_depth = prev_opens.size - line_tokens = [] + # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line. + # Example code + # ["hello + # world"+( + # First line + # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]] + # prev_opens: [] + # next_tokens: [lbracket, tstring_beg] + # min_depth: 0 (minimum at beginning of line) + # Second line + # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']] + # prev_opens: [lbracket, tstring_beg] + # next_tokens: [lbracket, lparen] + # min_depth: 1 (minimum just after tstring_end) + def parse_by_line(tokens) + line_tokens = [] + prev_opens = [] + min_depth = 0 + output = [] + last_opens = scan_opens(tokens) do |t, opens| + depth = t == opens.last&.first ? opens.size - 1 : opens.size + min_depth = depth if depth < min_depth + if t.tok.include?("\n") + t.tok.each_line do |line| + line_tokens << [t, line] + next if line[-1] != "\n" + next_opens = opens.map(&:first) + output << [line_tokens, prev_opens, next_opens, min_depth] + prev_opens = next_opens + min_depth = prev_opens.size + line_tokens = [] + end + else + line_tokens << [t, t.tok] end - else - line_tokens << [t, t.tok] end + output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any? + output end - output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any? - output end end end diff --git a/lib/irb/pager.rb b/lib/irb/pager.rb index 3391b32c66..558318cdb8 100644 --- a/lib/irb/pager.rb +++ b/lib/irb/pager.rb @@ -33,7 +33,11 @@ module IRB # the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager # So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process rescue IRB::Abort - Process.kill("TERM", pid) if pid + begin + Process.kill("TERM", pid) if pid + rescue Errno::ESRCH + # Pager process already terminated + end nil rescue Errno::EPIPE end diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index 86e340eb05..3abb53b4ea 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -36,29 +36,6 @@ module IRB :massign, ] - class TerminateLineInput < StandardError - def initialize - super("Terminate Line Input") - end - end - - def self.compile_with_errors_suppressed(code, line_no: 1) - begin - result = yield code, line_no - rescue ArgumentError - # Ruby can issue an error for the code if there is an - # incomplete magic comment for encoding in it. Force an - # expression with a new line before the code in this - # case to prevent magic comment handling. To make sure - # line numbers in the lexed code remain the same, - # decrease the line number by one. - code = ";\n#{code}" - line_no -= 1 - result = yield code, line_no - end - result - end - ERROR_TOKENS = [ :on_parse_error, :compile_error, @@ -68,70 +45,102 @@ module IRB :on_param_error ] - def self.generate_local_variables_assign_code(local_variables) - "#{local_variables.join('=')}=nil;" unless local_variables.empty? + LTYPE_TOKENS = %i[ + on_heredoc_beg on_tstring_beg + on_regexp_beg on_symbeg on_backtick + on_symbols_beg on_qsymbols_beg + on_words_beg on_qwords_beg + ] + + class TerminateLineInput < StandardError + def initialize + super("Terminate Line Input") + end end - # Some part of the code is not included in Ripper's token. - # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr. - # With interpolated tokens, tokens.map(&:tok).join will be equal to code. - def self.interpolate_ripper_ignored_tokens(code, tokens) - line_positions = [0] - code.lines.each do |line| - line_positions << line_positions.last + line.bytesize + class << self + def compile_with_errors_suppressed(code, line_no: 1) + begin + result = yield code, line_no + rescue ArgumentError + # Ruby can issue an error for the code if there is an + # incomplete magic comment for encoding in it. Force an + # expression with a new line before the code in this + # case to prevent magic comment handling. To make sure + # line numbers in the lexed code remain the same, + # decrease the line number by one. + code = ";\n#{code}" + line_no -= 1 + result = yield code, line_no + end + result + end + + def generate_local_variables_assign_code(local_variables) + "#{local_variables.join('=')}=nil;" unless local_variables.empty? end - prev_byte_pos = 0 - interpolated = [] - prev_line = 1 - tokens.each do |t| - line, col = t.pos - byte_pos = line_positions[line - 1] + col - if prev_byte_pos < byte_pos - tok = code.byteslice(prev_byte_pos...byte_pos) + + # Some part of the code is not included in Ripper's token. + # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr. + # With interpolated tokens, tokens.map(&:tok).join will be equal to code. + def interpolate_ripper_ignored_tokens(code, tokens) + line_positions = [0] + code.lines.each do |line| + line_positions << line_positions.last + line.bytesize + end + prev_byte_pos = 0 + interpolated = [] + prev_line = 1 + tokens.each do |t| + line, col = t.pos + byte_pos = line_positions[line - 1] + col + if prev_byte_pos < byte_pos + tok = code.byteslice(prev_byte_pos...byte_pos) + pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]] + interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0) + prev_line += tok.count("\n") + end + interpolated << t + prev_byte_pos = byte_pos + t.tok.bytesize + prev_line += t.tok.count("\n") + end + if prev_byte_pos < code.bytesize + tok = code.byteslice(prev_byte_pos..) pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]] interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0) - prev_line += tok.count("\n") end - interpolated << t - prev_byte_pos = byte_pos + t.tok.bytesize - prev_line += t.tok.count("\n") - end - if prev_byte_pos < code.bytesize - tok = code.byteslice(prev_byte_pos..) - pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]] - interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0) + interpolated end - interpolated - end - def self.ripper_lex_without_warning(code, local_variables: []) - verbose, $VERBOSE = $VERBOSE, nil - lvars_code = generate_local_variables_assign_code(local_variables) - original_code = code - if lvars_code - code = "#{lvars_code}\n#{code}" - line_no = 0 - else - line_no = 1 - end + def ripper_lex_without_warning(code, local_variables: []) + verbose, $VERBOSE = $VERBOSE, nil + lvars_code = generate_local_variables_assign_code(local_variables) + original_code = code + if lvars_code + code = "#{lvars_code}\n#{code}" + line_no = 0 + else + line_no = 1 + end - compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no| - lexer = Ripper::Lexer.new(inner_code, '-', line_no) - tokens = [] - lexer.scan.each do |t| - next if t.pos.first == 0 - prev_tk = tokens.last - position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize - if position_overlapped - tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event) - else - tokens << t + compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no| + lexer = Ripper::Lexer.new(inner_code, '-', line_no) + tokens = [] + lexer.scan.each do |t| + next if t.pos.first == 0 + prev_tk = tokens.last + position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize + if position_overlapped + tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event) + else + tokens << t + end end + interpolate_ripper_ignored_tokens(original_code, tokens) end - interpolate_ripper_ignored_tokens(original_code, tokens) + ensure + $VERBOSE = verbose end - ensure - $VERBOSE = verbose end def check_code_state(code, local_variables:) @@ -219,27 +228,6 @@ module IRB :unrecoverable_error rescue SyntaxError => e case e.message - when /unterminated (?:string|regexp) meets end of file/ - # "unterminated regexp meets end of file" - # - # example: - # / - # - # "unterminated string meets end of file" - # - # example: - # ' - return :recoverable_error - when /unexpected end-of-input/ - # "syntax error, unexpected end-of-input, expecting keyword_end" - # - # example: - # if true - # hoge - # if false - # fuga - # end - return :recoverable_error when /unexpected keyword_end/ # "syntax error, unexpected keyword_end" # @@ -262,6 +250,27 @@ module IRB # example: # method / f / return :unrecoverable_error + when /unterminated (?:string|regexp) meets end of file/ + # "unterminated regexp meets end of file" + # + # example: + # / + # + # "unterminated string meets end of file" + # + # example: + # ' + return :recoverable_error + when /unexpected end-of-input/ + # "syntax error, unexpected end-of-input, expecting keyword_end" + # + # example: + # if true + # hoge + # if false + # fuga + # end + return :recoverable_error else return :other_error end @@ -391,13 +400,6 @@ module IRB end end - LTYPE_TOKENS = %i[ - on_heredoc_beg on_tstring_beg - on_regexp_beg on_symbeg on_backtick - on_symbols_beg on_qsymbols_beg - on_words_beg on_qwords_beg - ] - def ltype_from_open_tokens(opens) start_token = opens.reverse_each.find do |tok| LTYPE_TOKENS.include?(tok.event) diff --git a/lib/irb/ruby_logo.aa b/lib/irb/ruby_logo.aa index 61fe22c94a..d0143a448b 100644 --- a/lib/irb/ruby_logo.aa +++ b/lib/irb/ruby_logo.aa @@ -1,41 +1,41 @@ -TYPE: LARGE +TYPE: ASCII_LARGE - -+smJYYN?mm- - HB"BBYT TQg NggT - 9Q+g Nm,T 8g NJW - YS+ N2NJ"Sg N? - BQg #( gT Nggggk J - 5j NJ NJ NNge - #Q #JJ NgT N( - @j bj mT J - Bj @/d NJ ( - #q #(( NgT #J - 5d #(t mT $d - #q @(@J NJB; - @( 5d ? HHH H HQmgggggggmN qD - 5d #uN 2QdH E O - 5 5JSd Nd NJH @d j - Fd @J4d s NQH #d ( - #( #o6d Nd NgH #d #d - 4 B&Od v NgT #d F - #( 9JGd NH NgUd F - #d #GJQ d NP $ - #J #U+#Q N Q # j - j /W BQ+ BQ d NJ NJ - - NjJH HBIjTQggPJQgW N W k #J - #J b HYWgggN j s Nag d NN b #d - #J 5- D s Ngg N d Nd F - Fd BKH2 #+ s NNgg J Q J ] - F H @ J N y K(d P I - F4 E N? #d y #Q NJ E j - F W Nd q m Bg NxW N(H- - F d b @ m Hd gW vKJ - NJ d K d s Bg aT FDd - b # d N m BQ mV N> - e5 Nd #d NggggggQWH HHHH NJ - - m7 NW H N HSVO1z=?11- - NgTH bB kH WBHWWHBHWmQgg&gggggNNN - NNggggggNN + ,,,;;;;;;;;;;;;;;;;;;;;;;,, + ,,,;;;;;;;;;,, ,;;;' ''';;, + ,,;;;''' ';;;, ,,;;'' '';, + ,;;'' ;;;;;;;;,,,,,, ';; + ,;;'' ;;;;';;;'''';;;;;;;;;,,,;; + ,,;'' ;;;; ';;, ''''';;, + ,;;' ;;;' ';;, ;; + ,;;' ,;;; '';,, ;; + ,;;' ;;; ';;, ,;; + ;;' ;;;' '';,, ;;; + ,;' ;;;; ';;, ;;' + ,;;' ,;;;;' ,,,,,,,,,,,,;;;;; + ,;' ,;;;;;;;;;;;;;;;;;;;;'''''''';;; + ;;' ,;;;;;;;;;,, ;;;; + ;;' ,;;;'' ;;, ';;,, ,;;;; + ;;' ,;;;' ;; '';;, ,;';;; + ;;' ,;;;' ;;, '';;,, ,;',;;; + ,;;; ,;;;' ;; '';;,, ,;' ;;;' + ;;;; ,,;;;' ;;, ';;;' ;;; +,;;; ,;;;;' ;; ,;;; ;;; +;;;;; ,,;;;;;' ;;, ,;';; ;;; +;;;;;, ,,;;;;;;;' ;; ,;;' ;;; ;;; +;;;;;;;,,,,,,,;;;;;;;;;;;;;;,,, ;;, ,;' ;; ;;; +;;' ;;;;;;;;;;'''' ,;';; ''';;;;,,, ;; ,;; ;; ;;; +;; ;;;'' ;; ';; ''';;;;,,,, ;;, ,;;' ;;, ;; +;; ;;;;, ;;' ';; ''';;;;,,;;;;' ';; ;; +;;;;;;';, ,;; ;; '';;;;, ;;,;; +;;; ;; ;;, ;; ;; ,;;' ';;, ;;;;; +;; ;;; ;;, ;;' ;; ,,;'' ';;, ;;;;; +;; ;; ;; ;; ;; ,;;' '';, ;;;; +;;,;; ;; ;;' ;; ,;;'' ';,, ;;;' + ;;;; ';; ,;; ;;,,;;'' ';;, ;;; + ';;; ';; ;; ,;;;;;;;;;;;;;,,,,,,,,,,,, ';;;;; + ';, ';,;;' ,,,;;'' '''''''';;;;;;;;;;;;;;;;;;; + ';;,,, ;;;; ,,,,;;;;;;,,,,,;;;;;;;;;;;;;;;;;;;'''''''''''''' + ''';;;;;;;;;;;;;;''''''''''''''' TYPE: ASCII ,,,;;;;''''';;;'';, ,,;'' ';;,;;; ', @@ -57,6 +57,44 @@ TYPE: ASCII ;;; '; ;' ';,,'' ';,;; '; ';,; ,,;''''''''';;;;;;,,;;; ';,,;;,,;;;;;;;;;;'''''''''''''' +TYPE: UNICODE_LARGE + + ⣀⣤⣴⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣤⣄⡀ + ⢀⣀⣤⣴⣾⣿⣿⣿⠿⣿⣿⣿⣿⣦⣀ ⢀⣤⣶⣿⠿⠛⠁⠈⠉⠙⠻⢿⣷⣦⡀ + ⢀⣠⣴⣾⡿⠿⠛⠉⠉ ⠈⠙⢿⣿⣷⣤⡀ ⣠⣴⣾⡿⠟⠉ ⠉⠻⣿⣦ + ⢀⣤⣶⣿⠟⠋⠁ ⢿⣿⣿⣿⣿⣿⣿⣧⣤⣤⣤⣀⣀⣀⡀ ⠘⢿⣷⡀ + ⢀⣠⣾⡿⠟⠉ ⢸⣿⣿⣿⠟⢿⣿⣯⡙⠛⠛⠛⠿⠿⠿⢿⣿⣿⣶⣶⣶⣦⣤⣬⣿⣧ + ⣠⣴⣿⠟⠋ ⢸⣿⣿⡿ ⠈⠻⣿⣶⣄ ⠉⠉⠉⠙⠛⢻⣿⡆ + ⣠⣾⡿⠛⠁ ⣼⣿⣿⠃ ⠈⠙⢿⣷⣤⡀ ⠈⣿⡇ + ⣠⣾⡿⠋ ⢠⣿⣿⡏ ⠙⠻⣿⣦⣀ ⣿⡇ + ⣠⣾⡿⠋ ⢀⣿⣿⣿ ⠈⠛⢿⣷⣄⡀ ⢠⣿⡇ + ⢀⣾⡿⠋ ⢀⣾⣿⣿⠇ ⠙⠻⣿⣦⣀ ⢸⣿⡇ + ⢀⣴⣿⠟⠁ ⢀⣾⣿⣿⡟ ⠈⠻⢿⣷⣄ ⣾⣿⠇ + ⢠⣾⡿⠃ ⣠⣿⣿⣿⣿⠃ ⣀⣀⣀⣀⣀⣀⣀⣀⣤⣤⣤⣤⣽⣿⣿⣿⣿ + ⣰⣿⠟ ⣴⣿⣿⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠿⠛⠛⠛⠛⠛⠛⠛⠛⣿⣿⣿ + ⣼⣿⠏ ⢠⣾⣿⣿⣿⡿⣿⣿⢿⣷⣦⣄ ⣼⣿⣿⣿ + ⣼⣿⠃ ⢀⣴⣿⣿⣿⠟⠋ ⢸⣿⡆⠈⠛⠿⣿⣦⣄⡀ ⣰⣿⣿⣿⡇ + ⢀⣾⣿⠃ ⢀⣴⣿⣿⣿⠟⠁ ⣿⣷ ⠈⠙⠻⣿⣶⣄⡀ ⣰⣿⠟⣿⣿⡇ + ⢀⣾⣿⠇ ⢀⣴⣿⣿⣿⠟⠁ ⢸⣿⡆ ⠙⠻⢿⣷⣤⣀ ⣰⣿⠏⢠⣿⣿⡇ + ⢠⣿⣿⡟ ⢀⣴⣿⣿⡿⠛⠁ ⣿⣷ ⠉⠻⢿⣷⣦⣀ ⣴⣿⠏ ⢸⣿⣿⠃ + ⣿⣿⣿⡇ ⣠⣴⣿⣿⡿⠋ ⢸⣿⡆ ⠈⠛⢿⣿⣿⠃ ⢸⣿⣿ +⢠⣿⣿⣿ ⢀⣴⣾⣿⣿⡿⠋ ⠈⣿⣧ ⢠⣾⣿⣿ ⢸⣿⣿ +⢸⣿⣿⣿⡇ ⣀⣴⣾⣿⣿⣿⡿⠋ ⢹⣿⡆ ⣴⣿⠟⢹⣿⡀ ⢸⣿⡿ +⢸⣿⡟⣿⣿⣄ ⣀⣤⣶⣿⣿⣿⣿⣿⡟⠉ ⠈⣿⣷ ⢠⣾⡿⠋ ⢸⣿⡇ ⣼⣿⡇ +⢸⣿⡇⢹⣿⣿⣷⣦⣤⣤⣤⣤⣤⣴⣶⣾⣿⣿⣿⣿⡿⠿⣿⣿⣿⣿⣷⣶⣤⣤⣀⡀ ⢹⣿⡆ ⢀⣴⣿⠟ ⣿⣧ ⣿⣿⡇ +⢸⣿⠃ ⢿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠛⠛⠉⠉⠁ ⢰⣿⠟⣿⣷⡀⠉⠙⠛⠿⢿⣿⣶⣦⣤⣀⡀ ⠈⣿⣷ ⣠⣿⡿⠁ ⢿⣿ ⣿⣿⡇ +⢸⣿ ⢀⣾⣿⣿⠋⠉⠁ ⢀⣿⡿ ⠘⣿⣷⡀ ⠉⠙⠛⠿⠿⣿⣶⣦⣤⣄⣀ ⢹⣿⡄ ⣠⣾⡿⠋ ⢸⣿⡆ ⣿⣿ +⣸⣿⢀⣾⣿⣿⣿⣆ ⣸⣿⠃ ⠘⢿⣷⡀ ⠈⠉⠛⠻⠿⣿⣷⣶⣤⣌⣿⣷⣾⡿⠋ ⠘⣿⡇ ⣿⣿ +⣿⣿⣾⡿⣿⡿⠹⣿⡆ ⢠⣿⡏ ⠈⢿⣷⡀ ⠈⠉⠙⣻⣿⣿⣿⣀ ⣿⣷⢰⣿⣿ +⣿⣿⡿⢁⣿⡇ ⢻⣿⡄ ⣾⣿ ⠈⢿⣷⡀ ⢀⣤⣾⡿⠋⠈⠻⢿⣷⣄ ⢻⣿⢸⣿⡟ +⣿⣿⠁⢸⣿⡇ ⢻⣿⡄ ⢸⣿⠇ ⠈⢿⣷⡀ ⣀⣴⣿⠟⠋ ⠙⢿⣷⣤⡀ ⢸⣿⣿⣿⡇ +⣿⣿ ⢸⣿⠁ ⠈⢿⣷⡀ ⢀⣿⡟ ⠈⢿⣷⡀ ⢀⣤⣾⡿⠛⠁ ⠙⠻⣿⣦⡀ ⠈⣿⣿⣿⡇ +⢸⣿⡄⣿⣿ ⠈⣿⣷⡀ ⣼⣿⠃ ⠈⢿⣷⡀ ⢀⣠⣶⣿⠟⠋ ⠈⠻⣿⣦⣄ ⣿⣿⣿⠇ +⠈⣿⣷⣿⡿ ⠘⣿⣧ ⢠⣿⡏ ⠈⢿⣷⣄⣤⣶⣿⠟⠋ ⠈⠛⢿⣷⣄ ⢸⣿⣿ + ⠘⣿⣿⡇ ⠘⣿⣧ ⣾⣿ ⢀⣠⣼⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣤⣤⣤⣤⣤⣤⣀⣀⣀⣀⣀⣀⡀ ⠙⢿⣷⣼⣿⣿ + ⠈⠻⣿⣦⡀ ⠹⣿⣆⢸⣿⠇ ⣀⣠⣴⣾⡿⠟⠋⠁ ⠉⠉⠉⠉⠉⠉⠛⠛⣛⣛⣛⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⡿ + ⠈⠻⢿⣷⣦⣄⣀⡀ ⢹⣿⣿⡟ ⢀⣀⣀⣤⣤⣶⣾⣿⣿⣿⣯⣥⣤⣤⣤⣤⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠟⠛⠛⠛⠛⠛⠛⠛⠉⠉⠉⠉⠉⠉ + ⠉⠙⠛⠿⠿⠿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠛⠛⠛⠛⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠉ TYPE: UNICODE ⣀⣤⣴⣾⣿⣿⣿⡛⠛⠛⠛⠛⣻⣿⠿⠛⠛⠶⣤⡀ ⣀⣴⠾⠛⠉⠁ ⠙⣿⣶⣤⣶⣟⣉ ⠈⠻⣦ diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb index 5d7d729d19..c515da5702 100644 --- a/lib/irb/source_finder.rb +++ b/lib/irb/source_finder.rb @@ -100,7 +100,7 @@ module IRB Source.new(file, line) elsif method # Method defined with eval, probably in IRB session - source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil + source = RubyVM::InstructionSequence.of(method)&.script_lines&.join rescue nil Source.new(file, line, source) end rescue EvaluationError diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb index a3391c12a3..9591a40357 100644 --- a/lib/irb/statement.rb +++ b/lib/irb/statement.rb @@ -68,7 +68,7 @@ module IRB end def suppresses_echo? - false + true end def should_be_handled_by_debugger? diff --git a/lib/irb/version.rb b/lib/irb/version.rb index c41917329c..955a3a81da 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -5,7 +5,7 @@ # module IRB # :nodoc: - VERSION = "1.13.1" + VERSION = "1.14.1" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2024-05-05" + @LAST_UPDATE_DATE = "2024-09-25" end diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb index d24d1cc38d..632b432439 100644 --- a/lib/irb/workspace.rb +++ b/lib/irb/workspace.rb @@ -176,11 +176,13 @@ EOF end module HelpersContainer - def self.install_helper_methods - HelperMethod.helper_methods.each do |name, helper_method_class| - define_method name do |*args, **opts, &block| - helper_method_class.instance.execute(*args, **opts, &block) - end unless method_defined?(name) + class << self + def install_helper_methods + HelperMethod.helper_methods.each do |name, helper_method_class| + define_method name do |*args, **opts, &block| + helper_method_class.instance.execute(*args, **opts, &block) + end unless method_defined?(name) + end end end diff --git a/lib/logger.rb b/lib/logger.rb index 4099955ef2..63179ec671 100644 --- a/lib/logger.rb +++ b/lib/logger.rb @@ -381,7 +381,7 @@ class Logger # Logging severity threshold (e.g. <tt>Logger::INFO</tt>). def level - @level_override[Fiber.current] || @level + level_override[Fiber.current] || @level end # Sets the log level; returns +severity+. @@ -406,14 +406,14 @@ class Logger # logger.debug { "Hello" } # end def with_level(severity) - prev, @level_override[Fiber.current] = level, Severity.coerce(severity) + prev, level_override[Fiber.current] = level, Severity.coerce(severity) begin yield ensure if prev - @level_override[Fiber.current] = prev + level_override[Fiber.current] = prev else - @level_override.delete(Fiber.current) + level_override.delete(Fiber.current) end end end @@ -574,10 +574,14 @@ class Logger # - +shift_period_suffix+: sets the format for the filename suffix # for periodic log file rotation; default is <tt>'%Y%m%d'</tt>. # See {Periodic Rotation}[rdoc-ref:Logger@Periodic+Rotation]. + # - +reraise_write_errors+: An array of exception classes, which will + # be reraised if there is an error when writing to the log device. + # The default is to swallow all exceptions raised. # def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG, progname: nil, formatter: nil, datetime_format: nil, - binmode: false, shift_period_suffix: '%Y%m%d') + binmode: false, shift_period_suffix: '%Y%m%d', + reraise_write_errors: []) self.level = level self.progname = progname @default_formatter = Formatter.new @@ -589,7 +593,8 @@ class Logger @logdev = LogDevice.new(logdev, shift_age: shift_age, shift_size: shift_size, shift_period_suffix: shift_period_suffix, - binmode: binmode) + binmode: binmode, + reraise_write_errors: reraise_write_errors) end end @@ -741,6 +746,11 @@ private SEV_LABEL[severity] || 'ANY' end + # Guarantee the existence of this ivar even when subclasses don't call the superclass constructor. + def level_override + @level_override ||= {} + end + def format_message(severity, datetime, progname, msg) (@formatter || @default_formatter).call(severity, datetime, progname, msg) end diff --git a/lib/logger/log_device.rb b/lib/logger/log_device.rb index 84277a2656..4876adf0b7 100644 --- a/lib/logger/log_device.rb +++ b/lib/logger/log_device.rb @@ -11,9 +11,10 @@ class Logger attr_reader :filename include MonitorMixin - def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false) + def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false, reraise_write_errors: []) @dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil @binmode = binmode + @reraise_write_errors = reraise_write_errors mon_initialize set_dev(log) if @filename @@ -34,16 +35,22 @@ class Logger if @shift_age and @dev.respond_to?(:stat) begin check_shift_log + rescue *@reraise_write_errors + raise rescue warn("log shifting failed. #{$!}") end end begin @dev.write(message) + rescue *@reraise_write_errors + raise rescue warn("log writing failed. #{$!}") end end + rescue *@reraise_write_errors + raise rescue Exception => ignored warn("log writing failed. #{ignored}") end diff --git a/lib/logger/period.rb b/lib/logger/period.rb index 0a291dbbbe..a0359defe3 100644 --- a/lib/logger/period.rb +++ b/lib/logger/period.rb @@ -8,14 +8,14 @@ class Logger def next_rotate_time(now, shift_age) case shift_age - when 'daily' + when 'daily', :daily t = Time.mktime(now.year, now.month, now.mday) + SiD - when 'weekly' + when 'weekly', :weekly t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday) - when 'monthly' + when 'monthly', :monthly t = Time.mktime(now.year, now.month, 1) + SiD * 32 return Time.mktime(t.year, t.month, 1) - when 'now', 'everytime' + when 'now', 'everytime', :now, :everytime return now else raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime" @@ -30,13 +30,13 @@ class Logger def previous_period_end(now, shift_age) case shift_age - when 'daily' + when 'daily', :daily t = Time.mktime(now.year, now.month, now.mday) - SiD / 2 - when 'weekly' + when 'weekly', :weekly t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2) - when 'monthly' + when 'monthly', :monthly t = Time.mktime(now.year, now.month, 1) - SiD / 2 - when 'now', 'everytime' + when 'now', 'everytime', :now, :everytime return now else raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime" diff --git a/lib/logger/version.rb b/lib/logger/version.rb index 202b6e4fba..2a0801be63 100644 --- a/lib/logger/version.rb +++ b/lib/logger/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Logger - VERSION = "1.6.0" + VERSION = "1.6.1" end diff --git a/lib/net/http.rb b/lib/net/http.rb index 6b78c264af..5cc9d2ce88 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -67,6 +67,8 @@ module Net #:nodoc: # Net::HTTP.post(uri, data) # params = {title: 'foo', body: 'bar', userId: 1} # Net::HTTP.post_form(uri, params) + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # Net::HTTP.put(uri, data) # # - If performance is important, consider using sessions, which lower request overhead. # This {session}[rdoc-ref:Net::HTTP@Sessions] has multiple requests for @@ -524,6 +526,8 @@ module Net #:nodoc: # Sends a POST request with form data and returns a response object. # - {::post}[rdoc-ref:Net::HTTP.post]: # Sends a POST request with data and returns a response object. + # - {::put}[rdoc-ref:Net::HTTP.put]: + # Sends a PUT request with data and returns a response object. # - {#copy}[rdoc-ref:Net::HTTP#copy]: # Sends a COPY request and returns a response object. # - {#delete}[rdoc-ref:Net::HTTP#delete]: @@ -893,6 +897,39 @@ module Net #:nodoc: } end + # Sends a PUT request to the server; returns a Net::HTTPResponse object. + # + # Argument +url+ must be a URL; + # argument +data+ must be a string: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # headers = {'content-type': 'application/json'} + # res = Net::HTTP.put(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": 1, + # "id": 101 + # } + # + # Related: + # + # - Net::HTTP::Put: request class for \HTTP method +PUT+. + # - Net::HTTP#put: convenience method for \HTTP method +PUT+. + # + def HTTP.put(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.put(url, data, header) + } + end + # # \HTTP session management # @@ -1066,7 +1103,7 @@ module Net #:nodoc: # For proxy-defining arguments +p_addr+ through +p_no_proxy+, # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. # - def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil) + def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil) http = super address, port if proxy_class? then # from Net::HTTP::Proxy() @@ -1075,6 +1112,7 @@ module Net #:nodoc: http.proxy_port = @proxy_port http.proxy_user = @proxy_user http.proxy_pass = @proxy_pass + http.proxy_use_ssl = @proxy_use_ssl elsif p_addr == :ENV then http.proxy_from_env = true else @@ -1086,34 +1124,67 @@ module Net #:nodoc: http.proxy_port = p_port || default_port http.proxy_user = p_user http.proxy_pass = p_pass + http.proxy_use_ssl = p_use_ssl end http end + class << HTTP + # Allows to set the default configuration that will be used + # when creating a new connection. + # + # Example: + # + # Net::HTTP.default_configuration = { + # read_timeout: 1, + # write_timeout: 1 + # } + # http = Net::HTTP.new(hostname) + # http.open_timeout # => 60 + # http.read_timeout # => 1 + # http.write_timeout # => 1 + # + attr_accessor :default_configuration + end + # Creates a new \Net::HTTP object for the specified server address, # without opening the TCP connection or initializing the \HTTP session. # The +address+ should be a DNS hostname or IP address. def initialize(address, port = nil) # :nodoc: + defaults = { + keep_alive_timeout: 2, + close_on_empty_response: false, + open_timeout: 60, + read_timeout: 60, + write_timeout: 60, + continue_timeout: nil, + max_retries: 1, + debug_output: nil, + response_body_encoding: false, + ignore_eof: true + } + options = defaults.merge(self.class.default_configuration || {}) + @address = address @port = (port || HTTP.default_port) @ipaddr = nil @local_host = nil @local_port = nil @curr_http_version = HTTPVersion - @keep_alive_timeout = 2 + @keep_alive_timeout = options[:keep_alive_timeout] @last_communicated = nil - @close_on_empty_response = false + @close_on_empty_response = options[:close_on_empty_response] @socket = nil @started = false - @open_timeout = 60 - @read_timeout = 60 - @write_timeout = 60 - @continue_timeout = nil - @max_retries = 1 - @debug_output = nil - @response_body_encoding = false - @ignore_eof = true + @open_timeout = options[:open_timeout] + @read_timeout = options[:read_timeout] + @write_timeout = options[:write_timeout] + @continue_timeout = options[:continue_timeout] + @max_retries = options[:max_retries] + @debug_output = options[:debug_output] + @response_body_encoding = options[:response_body_encoding] + @ignore_eof = options[:ignore_eof] @proxy_from_env = false @proxy_uri = nil @@ -1121,6 +1192,7 @@ module Net #:nodoc: @proxy_port = nil @proxy_user = nil @proxy_pass = nil + @proxy_use_ssl = nil @use_ssl = false @ssl_context = nil @@ -1255,6 +1327,7 @@ module Net #:nodoc: # Sets the proxy password; # see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server]. attr_writer :proxy_pass + attr_writer :proxy_use_ssl # Returns the IP address for the connection. # @@ -1444,23 +1517,6 @@ module Net #:nodoc: @use_ssl = flag end - SSL_IVNAMES = [ - :@ca_file, - :@ca_path, - :@cert, - :@cert_store, - :@ciphers, - :@extra_chain_cert, - :@key, - :@ssl_timeout, - :@ssl_version, - :@min_version, - :@max_version, - :@verify_callback, - :@verify_depth, - :@verify_mode, - :@verify_hostname, - ] # :nodoc: SSL_ATTRIBUTES = [ :ca_file, :ca_path, @@ -1479,6 +1535,8 @@ module Net #:nodoc: :verify_hostname, ] # :nodoc: + SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym } # :nodoc: + # Sets or returns the path to a CA certification file in PEM format. attr_accessor :ca_file @@ -1614,7 +1672,13 @@ module Net #:nodoc: debug "opened" if use_ssl? if proxy? - plain_sock = BufferedIO.new(s, read_timeout: @read_timeout, + if @proxy_use_ssl + proxy_sock = OpenSSL::SSL::SSLSocket.new(s) + ssl_socket_connect(proxy_sock, @open_timeout) + else + proxy_sock = s + end + proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout, write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) @@ -1625,8 +1689,8 @@ module Net #:nodoc: buf << "Proxy-Authorization: Basic #{credential}\r\n" end buf << "\r\n" - plain_sock.write(buf) - HTTPResponse.read_new(plain_sock).value + proxy_sock.write(buf) + HTTPResponse.read_new(proxy_sock).value # assuming nothing left in buffers after successful CONNECT response end @@ -1734,13 +1798,14 @@ module Net #:nodoc: @proxy_port = nil @proxy_user = nil @proxy_pass = nil + @proxy_use_ssl = nil # Creates an \HTTP proxy class which behaves like \Net::HTTP, but # performs all access via the specified proxy. # # This class is obsolete. You may pass these same parameters directly to # \Net::HTTP.new. See Net::HTTP.new for details of the arguments. - def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) #:nodoc: + def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc: return self unless p_addr Class.new(self) { @@ -1758,6 +1823,7 @@ module Net #:nodoc: @proxy_user = p_user @proxy_pass = p_pass + @proxy_use_ssl = p_use_ssl } end @@ -1782,6 +1848,9 @@ module Net #:nodoc: # Returns the password for accessing the proxy, or +nil+ if none; # see Net::HTTP@Proxy+Server. attr_reader :proxy_pass + + # Use SSL when talking to the proxy. If Net::HTTP does not use a proxy, nil. + attr_reader :proxy_use_ssl end # Returns +true+ if a proxy server is defined, +false+ otherwise; @@ -2016,6 +2085,11 @@ module Net #:nodoc: # http = Net::HTTP.new(hostname) # http.put('/todos/1', data) # => #<Net::HTTPOK 200 OK readbody=true> # + # Related: + # + # - Net::HTTP::Put: request class for \HTTP method PUT. + # - Net::HTTP.put: sends PUT request, returns response body. + # def put(path, data, initheader = nil) request(Put.new(path, initheader), data) end diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb index 6660c8075a..f6c36f1b5e 100644 --- a/lib/net/http/header.rb +++ b/lib/net/http/header.rb @@ -491,7 +491,7 @@ module Net::HTTPHeader alias canonical_each each_capitalized def capitalize(name) - name.to_s.split(/-/).map {|s| s.capitalize }.join('-') + name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze) end private :capitalize diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb index 5724164205..e58057adf1 100644 --- a/lib/net/http/requests.rb +++ b/lib/net/http/requests.rb @@ -124,6 +124,11 @@ end # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. # +# Related: +# +# - Net::HTTP.put: sends +PUT+ request, returns response object. +# - Net::HTTP#put: sends +PUT+ request, returns response object. +# class Net::HTTP::Put < Net::HTTPRequest METHOD = 'PUT' REQUEST_HAS_BODY = true diff --git a/lib/open-uri.rb b/lib/open-uri.rb index ba2379325f..f2eddbcd2b 100644 --- a/lib/open-uri.rb +++ b/lib/open-uri.rb @@ -109,6 +109,7 @@ module OpenURI :redirect => true, :encoding => nil, :max_redirects => 64, + :request_specific_fields => nil, } def OpenURI.check_options(options) # :nodoc: @@ -148,7 +149,11 @@ module OpenURI end encoding = Encoding.find(options[:encoding]) end - + if options.has_key? :request_specific_fields + if !(options[:request_specific_fields].is_a?(Hash) || options[:request_specific_fields].is_a?(Proc)) + raise ArgumentError, "Invalid request_specific_fields option: #{options[:request_specific_fields].inspect}" + end + end unless mode == nil || mode == 'r' || mode == 'rb' || mode == File::RDONLY @@ -212,12 +217,20 @@ module OpenURI end uri_set = {} - max_redirects = options[:max_redirects] + max_redirects = options[:max_redirects] || Options.fetch(:max_redirects) buf = nil while true + request_specific_fields = {} + if options.has_key? :request_specific_fields + request_specific_fields = if options[:request_specific_fields].is_a?(Hash) + options[:request_specific_fields] + else options[:request_specific_fields].is_a?(Proc) + options[:request_specific_fields].call(uri) + end + end redirect = catch(:open_uri_redirect) { buf = Buffer.new - uri.buffer_open(buf, find_proxy.call(uri), options) + uri.buffer_open(buf, find_proxy.call(uri), options.merge(request_specific_fields)) nil } if redirect @@ -237,6 +250,10 @@ module OpenURI options = options.dup options.delete :http_basic_authentication end + if options.include?(:request_specific_fields) && options[:request_specific_fields].is_a?(Hash) + # Send request specific headers only for the initial request. + options.delete :request_specific_fields + end uri = redirect raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s uri_set[uri.to_s] = true @@ -746,6 +763,44 @@ module OpenURI # Using +true+ also means that redirections between http and ftp are # permitted. # + # [:max_redirects] + # Synopsis: + # :max_redirects=>int + # + # Number of HTTP redirects allowed before OpenURI::TooManyRedirects is raised. + # The default is 64. + # + # [:request_specific_fields] + # Synopsis: + # :request_specific_fields => {} + # :request_specific_fields => lambda {|url| ...} + # + # :request_specific_fields option allows specifying custom header fields that + # are sent with the HTTP request. It can be passed as a Hash or a Proc that + # gets evaluated on each request and returns a Hash of header fields. + # + # If a Hash is provided, it specifies the headers only for the initial + # request and these headers will not be sent on redirects. + # + # If a Proc is provided, it will be executed for each request including + # redirects, allowing dynamic header customization based on the request URL. + # It is important that the Proc returns a Hash. And this Hash specifies the + # headers to be sent with the request. + # + # For Example with Hash + # URI.open("http://...", + # request_specific_fields: {"Authorization" => "token dummy"}) {|f| ... } + # + # For Example with Proc: + # URI.open("http://...", + # request_specific_fields: lambda { |uri| + # if uri.host == "example.com" + # {"Authorization" => "token dummy"} + # else + # {} + # end + # }) {|f| ... } + # def open(*rest, &block) OpenURI.open_uri(self, *rest, &block) end diff --git a/lib/optparse.rb b/lib/optparse.rb index 069c3e436e..50641867f0 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1115,7 +1115,7 @@ XXX Switch::OptionalArgument.new do |pkg| if pkg begin - require 'optparse/version' + require_relative 'optparse/version' rescue LoadError else show_version(*pkg.split(/,/)) or @@ -1729,9 +1729,9 @@ XXX end end begin - opt, cb, *val = sw.parse(rest, argv) {|*exc| raise(*exc)} - val = callback!(cb, 1, *val) if cb - callback!(setter, 2, sw.switch_name, *val) if setter + opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)} + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, rest) end @@ -1761,7 +1761,7 @@ XXX raise $!.set_option(arg, true) end begin - opt, cb, *val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} + opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} rescue ParseError raise $!.set_option(arg, arg.length > 2) else @@ -1769,8 +1769,8 @@ XXX end begin argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-') - val = callback!(cb, 1, *val) if cb - callback!(setter, 2, sw.switch_name, *val) if setter + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, arg.length > 2) end @@ -1798,6 +1798,8 @@ XXX # Calls callback with _val_. def callback!(cb, max_arity, *args) # :nodoc: + args.compact! + if (size = args.size) < max_arity and cb.to_proc.lambda? (arity = cb.arity) < 0 and arity = (1-arity) arity = max_arity if arity > max_arity diff --git a/lib/prism.rb b/lib/prism.rb index 19774538e7..66a64e7fd0 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -13,7 +13,6 @@ module Prism autoload :BasicVisitor, "prism/visitor" autoload :Compiler, "prism/compiler" - autoload :Debug, "prism/debug" autoload :DesugarCompiler, "prism/desugar_compiler" autoload :Dispatcher, "prism/dispatcher" autoload :DotVisitor, "prism/dot_visitor" @@ -32,7 +31,6 @@ module Prism # Some of these constants are not meant to be exposed, so marking them as # private here. - private_constant :Debug private_constant :LexCompat private_constant :LexRipper @@ -71,8 +69,6 @@ require_relative "prism/polyfill/byteindex" require_relative "prism/node" require_relative "prism/node_ext" require_relative "prism/parse_result" -require_relative "prism/parse_result/comments" -require_relative "prism/parse_result/newlines" # This is a Ruby implementation of the prism parser. If we're running on CRuby # and we haven't explicitly set the PRISM_FFI_BACKEND environment variable, then diff --git a/lib/prism/debug.rb b/lib/prism/debug.rb deleted file mode 100644 index 74f824faa7..0000000000 --- a/lib/prism/debug.rb +++ /dev/null @@ -1,249 +0,0 @@ -# frozen_string_literal: true - -module Prism - # This module is used for testing and debugging and is not meant to be used by - # consumers of this library. - module Debug - # A wrapper around a RubyVM::InstructionSequence that provides a more - # convenient interface for accessing parts of the iseq. - class ISeq # :nodoc: - attr_reader :parts - - def initialize(parts) - @parts = parts - end - - def type - parts[0] - end - - def local_table - parts[10] - end - - def instructions - parts[13] - end - - def each_child - instructions.each do |instruction| - # Only look at arrays. Other instructions are line numbers or - # tracepoint events. - next unless instruction.is_a?(Array) - - instruction.each do |opnd| - # Only look at arrays. Other operands are literals. - next unless opnd.is_a?(Array) - - # Only look at instruction sequences. Other operands are literals. - next unless opnd[0] == "YARVInstructionSequence/SimpleDataFormat" - - yield ISeq.new(opnd) - end - end - end - end - - private_constant :ISeq - - # :call-seq: - # Debug::cruby_locals(source) -> Array - # - # For the given source, compiles with CRuby and returns a list of all of the - # sets of local variables that were encountered. - def self.cruby_locals(source) - verbose, $VERBOSE = $VERBOSE, nil - - begin - locals = [] #: Array[Array[Symbol | Integer]] - stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)] - - while (iseq = stack.pop) - names = [*iseq.local_table] - names.map!.with_index do |name, index| - # When an anonymous local variable is present in the iseq's local - # table, it is represented as the stack offset from the top. - # However, when these are dumped to binary and read back in, they - # are replaced with the symbol :#arg_rest. To consistently handle - # this, we replace them here with their index. - if name == :"#arg_rest" - names.length - index + 1 - else - name - end - end - - locals << names - iseq.each_child { |child| stack << child } - end - - locals - ensure - $VERBOSE = verbose - end - end - - # Used to hold the place of a local that will be in the local table but - # cannot be accessed directly from the source code. For example, the - # iteration variable in a for loop or the positional parameter on a method - # definition that is destructured. - AnonymousLocal = Object.new - private_constant :AnonymousLocal - - # :call-seq: - # Debug::prism_locals(source) -> Array - # - # For the given source, parses with prism and returns a list of all of the - # sets of local variables that were encountered. - def self.prism_locals(source) - locals = [] #: Array[Array[Symbol | Integer]] - stack = [Prism.parse(source).value] #: Array[Prism::node] - - while (node = stack.pop) - case node - when BlockNode, DefNode, LambdaNode - names = node.locals - params = - if node.is_a?(DefNode) - node.parameters - elsif node.parameters.is_a?(NumberedParametersNode) - nil - else - node.parameters&.parameters - end - - # prism places parameters in the same order that they appear in the - # source. CRuby places them in the order that they need to appear - # according to their own internal calling convention. We mimic that - # order here so that we can compare properly. - if params - sorted = [ - *params.requireds.map do |required| - if required.is_a?(RequiredParameterNode) - required.name - else - AnonymousLocal - end - end, - *params.optionals.map(&:name), - *((params.rest.name || :*) if params.rest && !params.rest.is_a?(ImplicitRestNode)), - *params.posts.map do |post| - if post.is_a?(RequiredParameterNode) - post.name - else - AnonymousLocal - end - end, - *params.keywords.grep(RequiredKeywordParameterNode).map(&:name), - *params.keywords.grep(OptionalKeywordParameterNode).map(&:name), - ] - - sorted << AnonymousLocal if params.keywords.any? - - if params.keyword_rest.is_a?(ForwardingParameterNode) - sorted.push(:*, :**, :&, :"...") - elsif params.keyword_rest.is_a?(KeywordRestParameterNode) - sorted << (params.keyword_rest.name || :**) - end - - # Recurse down the parameter tree to find any destructured - # parameters and add them after the other parameters. - param_stack = params.requireds.concat(params.posts).grep(MultiTargetNode).reverse - while (param = param_stack.pop) - case param - when MultiTargetNode - param_stack.concat(param.rights.reverse) - param_stack << param.rest if param.rest&.expression && !sorted.include?(param.rest.expression.name) - param_stack.concat(param.lefts.reverse) - when RequiredParameterNode - sorted << param.name - when SplatNode - sorted << param.expression.name - end - end - - if params.block - sorted << (params.block.name || :&) - end - - names = sorted.concat(names - sorted) - end - - names.map!.with_index do |name, index| - if name == AnonymousLocal - names.length - index + 1 - else - name - end - end - - locals << names - when ClassNode, ModuleNode, ProgramNode, SingletonClassNode - locals << node.locals - when ForNode - locals << [2] - when PostExecutionNode - locals.push([], []) - when InterpolatedRegularExpressionNode - locals << [] if node.once? - end - - stack.concat(node.compact_child_nodes) - end - - locals - end - - # :call-seq: - # Debug::newlines(source) -> Array - # - # For the given source string, return the byte offsets of every newline in - # the source. - def self.newlines(source) - Prism.parse(source).source.offsets - end - - # A wrapping around prism's internal encoding data structures. This is used - # for reflection and debugging purposes. - class Encoding - # The name of the encoding, that can be passed to Encoding.find. - attr_reader :name - - # Initialize a new encoding with the given name and whether or not it is - # a multibyte encoding. - def initialize(name, multibyte) - @name = name - @multibyte = multibyte - end - - # Whether or not the encoding is a multibyte encoding. - def multibyte? - @multibyte - end - - # Returns the number of bytes of the first character in the source string, - # if it is valid for the encoding. Otherwise, returns 0. - def width(source) - Encoding._width(name, source) - end - - # Returns true if the first character in the source string is a valid - # alphanumeric character for the encoding. - def alnum?(source) - Encoding._alnum?(name, source) - end - - # Returns true if the first character in the source string is a valid - # alphabetic character for the encoding. - def alpha?(source) - Encoding._alpha?(name, source) - end - - # Returns true if the first character in the source string is a valid - # uppercase character for the encoding. - def upper?(source) - Encoding._upper?(name, source) - end - end - end -end diff --git a/lib/prism/desugar_compiler.rb b/lib/prism/desugar_compiler.rb index de02445149..e3b15fc3b0 100644 --- a/lib/prism/desugar_compiler.rb +++ b/lib/prism/desugar_compiler.rb @@ -2,11 +2,13 @@ module Prism class DesugarAndWriteNode # :nodoc: - attr_reader :node, :source, :read_class, :write_class, :arguments + include DSL - def initialize(node, source, read_class, write_class, *arguments) + attr_reader :node, :default_source, :read_class, :write_class, :arguments + + def initialize(node, default_source, read_class, write_class, **arguments) @node = node - @source = source + @default_source = default_source @read_class = read_class @write_class = write_class @arguments = arguments @@ -14,22 +16,30 @@ module Prism # Desugar `x &&= y` to `x && x = y` def compile - AndNode.new( - source, - read_class.new(source, *arguments, node.name_loc), - write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location), - node.operator_loc, - node.location + and_node( + location: node.location, + left: public_send(read_class, location: node.name_loc, **arguments), + right: public_send( + write_class, + location: node.location, + **arguments, + name_loc: node.name_loc, + value: node.value, + operator_loc: node.operator_loc + ), + operator_loc: node.operator_loc ) end end class DesugarOrWriteDefinedNode # :nodoc: - attr_reader :node, :source, :read_class, :write_class, :arguments + include DSL + + attr_reader :node, :default_source, :read_class, :write_class, :arguments - def initialize(node, source, read_class, write_class, *arguments) + def initialize(node, default_source, read_class, write_class, **arguments) @node = node - @source = source + @default_source = default_source @read_class = read_class @write_class = write_class @arguments = arguments @@ -37,35 +47,50 @@ module Prism # Desugar `x ||= y` to `defined?(x) ? x : x = y` def compile - IfNode.new( - source, - node.operator_loc, - DefinedNode.new(source, nil, read_class.new(source, *arguments, node.name_loc), nil, node.operator_loc, node.name_loc), - node.operator_loc, - StatementsNode.new(source, [read_class.new(source, *arguments, node.name_loc)], node.location), - ElseNode.new( - source, - node.operator_loc, - StatementsNode.new( - source, - [write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location)], - node.location + if_node( + location: node.location, + if_keyword_loc: node.operator_loc, + predicate: defined_node( + location: node.name_loc, + value: public_send(read_class, location: node.name_loc, **arguments), + keyword_loc: node.operator_loc + ), + then_keyword_loc: node.operator_loc, + statements: statements_node( + location: node.location, + body: [public_send(read_class, location: node.name_loc, **arguments)] + ), + subsequent: else_node( + location: node.location, + else_keyword_loc: node.operator_loc, + statements: statements_node( + location: node.location, + body: [ + public_send( + write_class, + location: node.location, + **arguments, + name_loc: node.name_loc, + value: node.value, + operator_loc: node.operator_loc + ) + ] ), - node.operator_loc, - node.location + end_keyword_loc: node.operator_loc ), - node.operator_loc, - node.location + end_keyword_loc: node.operator_loc ) end end class DesugarOperatorWriteNode # :nodoc: - attr_reader :node, :source, :read_class, :write_class, :arguments + include DSL - def initialize(node, source, read_class, write_class, *arguments) + attr_reader :node, :default_source, :read_class, :write_class, :arguments + + def initialize(node, default_source, read_class, write_class, **arguments) @node = node - @source = source + @default_source = default_source @read_class = read_class @write_class = write_class @arguments = arguments @@ -75,35 +100,41 @@ module Prism def compile binary_operator_loc = node.binary_operator_loc.chop - write_class.new( - source, - *arguments, - node.name_loc, - CallNode.new( - source, - 0, - read_class.new(source, *arguments, node.name_loc), - nil, - binary_operator_loc.slice.to_sym, - binary_operator_loc, - nil, - ArgumentsNode.new(source, 0, [node.value], node.value.location), - nil, - nil, - node.location + public_send( + write_class, + location: node.location, + **arguments, + name_loc: node.name_loc, + value: call_node( + location: node.location, + receiver: public_send( + read_class, + location: node.name_loc, + **arguments + ), + name: binary_operator_loc.slice.to_sym, + message_loc: binary_operator_loc, + arguments: arguments_node( + location: node.value.location, + arguments: [node.value] + ) ), - node.binary_operator_loc.copy(start_offset: node.binary_operator_loc.end_offset - 1, length: 1), - node.location + operator_loc: node.binary_operator_loc.copy( + start_offset: node.binary_operator_loc.end_offset - 1, + length: 1 + ) ) end end class DesugarOrWriteNode # :nodoc: - attr_reader :node, :source, :read_class, :write_class, :arguments + include DSL - def initialize(node, source, read_class, write_class, *arguments) + attr_reader :node, :default_source, :read_class, :write_class, :arguments + + def initialize(node, default_source, read_class, write_class, **arguments) @node = node - @source = source + @default_source = default_source @read_class = read_class @write_class = write_class @arguments = arguments @@ -111,12 +142,18 @@ module Prism # Desugar `x ||= y` to `x || x = y` def compile - OrNode.new( - source, - read_class.new(source, *arguments, node.name_loc), - write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location), - node.operator_loc, - node.location + or_node( + location: node.location, + left: public_send(read_class, location: node.name_loc, **arguments), + right: public_send( + write_class, + location: node.location, + **arguments, + name_loc: node.name_loc, + value: node.value, + operator_loc: node.operator_loc + ), + operator_loc: node.operator_loc ) end end @@ -125,91 +162,91 @@ module Prism class ClassVariableAndWriteNode def desugar # :nodoc: - DesugarAndWriteNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile + DesugarAndWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ClassVariableOrWriteNode def desugar # :nodoc: - DesugarOrWriteDefinedNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile + DesugarOrWriteDefinedNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ClassVariableOperatorWriteNode def desugar # :nodoc: - DesugarOperatorWriteNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile + DesugarOperatorWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile end end class ConstantAndWriteNode def desugar # :nodoc: - DesugarAndWriteNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile + DesugarAndWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class ConstantOrWriteNode def desugar # :nodoc: - DesugarOrWriteDefinedNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile + DesugarOrWriteDefinedNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class ConstantOperatorWriteNode def desugar # :nodoc: - DesugarOperatorWriteNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile + DesugarOperatorWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile end end class GlobalVariableAndWriteNode def desugar # :nodoc: - DesugarAndWriteNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile + DesugarAndWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class GlobalVariableOrWriteNode def desugar # :nodoc: - DesugarOrWriteDefinedNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile + DesugarOrWriteDefinedNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class GlobalVariableOperatorWriteNode def desugar # :nodoc: - DesugarOperatorWriteNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile + DesugarOperatorWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile end end class InstanceVariableAndWriteNode def desugar # :nodoc: - DesugarAndWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile + DesugarAndWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class InstanceVariableOrWriteNode def desugar # :nodoc: - DesugarOrWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile + DesugarOrWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class InstanceVariableOperatorWriteNode def desugar # :nodoc: - DesugarOperatorWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile + DesugarOperatorWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile end end class LocalVariableAndWriteNode def desugar # :nodoc: - DesugarAndWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile + DesugarAndWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end end class LocalVariableOrWriteNode def desugar # :nodoc: - DesugarOrWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile + DesugarOrWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end end class LocalVariableOperatorWriteNode def desugar # :nodoc: - DesugarOperatorWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile + DesugarOperatorWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile end end diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index b62a59d037..0520f7cdd2 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -72,6 +72,7 @@ module Prism end callback :pm_parse_stream_fgets_t, [:pointer, :int, :pointer], :pointer + enum :pm_string_init_result_t, %i[PM_STRING_INIT_SUCCESS PM_STRING_INIT_ERROR_GENERIC PM_STRING_INIT_ERROR_DIRECTORY] load_exported_functions_from( "prism.h", @@ -176,13 +177,26 @@ module Prism def self.with_file(filepath) raise TypeError unless filepath.is_a?(String) + # On Windows and Mac, it's expected that filepaths will be encoded in + # UTF-8. If they are not, we need to convert them to UTF-8 before + # passing them into pm_string_mapped_init. + if RbConfig::CONFIG["host_os"].match?(/bccwin|cygwin|djgpp|mingw|mswin|wince|darwin/i) && + (encoding = filepath.encoding) != Encoding::ASCII_8BIT && encoding != Encoding::UTF_8 + filepath = filepath.encode(Encoding::UTF_8) + end + FFI::MemoryPointer.new(SIZEOF) do |pm_string| - if LibRubyParser.pm_string_mapped_init(pm_string, filepath) + case (result = LibRubyParser.pm_string_mapped_init(pm_string, filepath)) + when :PM_STRING_INIT_SUCCESS pointer = LibRubyParser.pm_string_source(pm_string) length = LibRubyParser.pm_string_length(pm_string) return yield new(pointer, length, false) - else + when :PM_STRING_INIT_ERROR_GENERIC raise SystemCallError.new(filepath, FFI.errno) + when :PM_STRING_INIT_ERROR_DIRECTORY + raise Errno::EISDIR.new(filepath) + else + raise "Unknown error initializing pm_string_t: #{result.inspect}" end ensure LibRubyParser.pm_string_free(pm_string) @@ -200,8 +214,8 @@ module Prism class << self # Mirror the Prism.dump API by using the serialization API. - def dump(code, **options) - LibRubyParser::PrismString.with_string(code) { |string| dump_common(string, options) } + def dump(source, **options) + LibRubyParser::PrismString.with_string(source) { |string| dump_common(string, options) } end # Mirror the Prism.dump_file API by using the serialization API. @@ -302,6 +316,27 @@ module Prism !parse_file_success?(filepath, **options) end + # Mirror the Prism.profile API by using the serialization API. + def profile(source, **options) + LibRubyParser::PrismString.with_string(source) do |string| + LibRubyParser::PrismBuffer.with do |buffer| + LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options)) + nil + end + end + end + + # Mirror the Prism.profile_file API by using the serialization API. + def profile_file(filepath, **options) + LibRubyParser::PrismString.with_file(filepath) do |string| + LibRubyParser::PrismBuffer.with do |buffer| + options[:filepath] = filepath + LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options)) + nil + end + end + end + private def dump_common(string, options) # :nodoc: @@ -376,6 +411,20 @@ module Prism end end + # Return the value that should be dumped for the version option. + def dump_options_version(version) + case version + when nil, "latest" + 0 + when /\A3\.3(\.\d+)?\z/ + 1 + when /\A3\.4(\.\d+)?\z/ + 0 + else + raise ArgumentError, "invalid version: #{version}" + end + end + # Convert the given options into a serialized options string. def dump_options(options) template = +"" @@ -394,7 +443,7 @@ module Prism template << "L" if (encoding = options[:encoding]) - name = encoding.name + name = encoding.is_a?(Encoding) ? encoding.name : encoding values.push(name.bytesize, name.b) template << "A*" else @@ -408,7 +457,16 @@ module Prism values << dump_options_command_line(options) template << "C" - values << { nil => 0, "3.3.0" => 1, "3.3.1" => 1, "3.4.0" => 0, "latest" => 0 }.fetch(options[:version]) + values << dump_options_version(options[:version]) + + template << "C" + values << (options[:encoding] == false ? 1 : 0) + + template << "C" + values << (options.fetch(:main_script, false) ? 1 : 0) + + template << "C" + values << (options.fetch(:partial_script, false) ? 1 : 0) template << "L" if (scopes = options[:scopes]) diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 4f8e443a3b..a83c24cb41 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -481,7 +481,7 @@ module Prism embexpr_balance -= 1 when :on_tstring_content if embexpr_balance == 0 - while index < max_index && tokens[index].event == :on_tstring_content + while index < max_index && tokens[index].event == :on_tstring_content && !token.value.match?(/\\\r?\n\z/) token.value << tokens[index].value index += 1 end diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index ceec76b8d6..4dfcebd638 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -5,10 +5,13 @@ module Prism class Node def deprecated(*replacements) # :nodoc: + location = caller_locations(1, 1) + location = location[0].label if location suggest = replacements.map { |replacement| "#{self.class}##{replacement}" } + warn(<<~MSG, category: :deprecated) - [deprecation]: #{self.class}##{caller_locations(1, 1)[0].label} is deprecated \ - and will be removed in the next major version. Use #{suggest.join("/")} instead. + [deprecation]: #{self.class}##{location} is deprecated and will be \ + removed in the next major version. Use #{suggest.join("/")} instead. #{(caller(1, 3) || []).join("\n")} MSG end @@ -18,7 +21,10 @@ module Prism # Returns a numeric value that represents the flags that were used to create # the regular expression. def options - o = flags & (RegularExpressionFlags::IGNORE_CASE | RegularExpressionFlags::EXTENDED | RegularExpressionFlags::MULTI_LINE) + o = 0 + o |= Regexp::IGNORECASE if flags.anybits?(RegularExpressionFlags::IGNORE_CASE) + o |= Regexp::EXTENDED if flags.anybits?(RegularExpressionFlags::EXTENDED) + o |= Regexp::MULTILINE if flags.anybits?(RegularExpressionFlags::MULTI_LINE) o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8) o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT) o @@ -66,11 +72,12 @@ module Prism def to_interpolated InterpolatedStringNode.new( source, + -1, + location, frozen? ? InterpolatedStringNodeFlags::FROZEN : 0, opening_loc, - [copy(opening_loc: nil, closing_loc: nil, location: content_loc)], - closing_loc, - location + [copy(location: content_loc, opening_loc: nil, closing_loc: nil)], + closing_loc ) end end @@ -83,10 +90,12 @@ module Prism def to_interpolated InterpolatedXStringNode.new( source, + -1, + location, + flags, opening_loc, - [StringNode.new(source, 0, nil, content_loc, nil, unescaped, content_loc)], - closing_loc, - location + [StringNode.new(source, node_id, content_loc, 0, nil, content_loc, nil, unescaped)], + closing_loc ) end end @@ -103,7 +112,19 @@ module Prism class RationalNode < Node # Returns the value of the node as a Ruby Rational. def value - Rational(numeric.is_a?(IntegerNode) ? numeric.value : slice.chomp("r")) + Rational(numerator, denominator) + end + + # Returns the value of the node as an IntegerNode or a FloatNode. This + # method is deprecated in favor of #value or #numerator/#denominator. + def numeric + deprecated("value", "numerator", "denominator") + + if denominator == 1 + IntegerNode.new(source, -1, location.chop, flags, numerator) + else + FloatNode.new(source, -1, location.chop, 0, numerator.to_f / denominator) + end end end @@ -180,7 +201,12 @@ module Prism # continue to supply that API. def child deprecated("name", "name_loc") - name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location) + + if name + ConstantReadNode.new(source, -1, name_loc, 0, name) + else + MissingNode.new(source, -1, location, 0) + end end end @@ -216,7 +242,12 @@ module Prism # continue to supply that API. def child deprecated("name", "name_loc") - name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location) + + if name + ConstantReadNode.new(source, -1, name_loc, 0, name) + else + MissingNode.new(source, -1, location, 0) + end end end @@ -249,9 +280,10 @@ module Prism end posts.each do |param| - if param.is_a?(MultiTargetNode) + case param + when MultiTargetNode names << [:req] - elsif param.is_a?(NoKeywordsParameterNode) + when NoKeywordsParameterNode, KeywordRestParameterNode, ForwardingParameterNode # Invalid syntax, e.g. "def f(**nil, ...)" moves the NoKeywordsParameterNode to posts raise "Invalid syntax" else @@ -428,4 +460,49 @@ module Prism binary_operator_loc end end + + class CaseMatchNode < Node + # Returns the else clause of the case match node. This method is deprecated + # in favor of #else_clause. + def consequent + deprecated("else_clause") + else_clause + end + end + + class CaseNode < Node + # Returns the else clause of the case node. This method is deprecated in + # favor of #else_clause. + def consequent + deprecated("else_clause") + else_clause + end + end + + class IfNode < Node + # Returns the subsequent if/elsif/else clause of the if node. This method is + # deprecated in favor of #subsequent. + def consequent + deprecated("subsequent") + subsequent + end + end + + class RescueNode < Node + # Returns the subsequent rescue clause of the rescue node. This method is + # deprecated in favor of #subsequent. + def consequent + deprecated("subsequent") + subsequent + end + end + + class UnlessNode < Node + # Returns the else clause of the unless node. This method is deprecated in + # favor of #else_clause. + def consequent + deprecated("else_clause") + else_clause + end + end end diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 63cc72a966..ae026b42ac 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -10,7 +10,11 @@ module Prism # specialized and more performant `ASCIISource` if no multibyte characters # are present in the source code. def self.for(source, start_line = 1, offsets = []) - source.ascii_only? ? ASCIISource.new(source, start_line, offsets): new(source, start_line, offsets) + if source.ascii_only? + ASCIISource.new(source, start_line, offsets) + else + new(source, start_line, offsets) + end end # The source code that this source object represents. @@ -87,7 +91,12 @@ module Prism # encodings, it is not captured here. def code_units_offset(byte_offset, encoding) byteslice = (source.byteslice(0, byte_offset) or raise).encode(encoding) - (encoding == Encoding::UTF_16LE || encoding == Encoding::UTF_16BE) ? (byteslice.bytesize / 2) : byteslice.length + + if encoding == Encoding::UTF_16LE || encoding == Encoding::UTF_16BE + byteslice.bytesize / 2 + else + byteslice.length + end end # Returns the column number in code units for the given encoding for the @@ -146,7 +155,7 @@ module Prism # Specialized version of `code_units_column` that does not depend on # `code_units_offset`, which is a more expensive operation. This is - # essentialy the same as `Prism::Source#column`. + # essentially the same as `Prism::Source#column`. def code_units_column(byte_offset, encoding) byte_offset - line_start(byte_offset) end @@ -574,6 +583,14 @@ module Prism # This is a result specific to the `parse` and `parse_file` methods. class ParseResult < Result + autoload :Comments, "prism/parse_result/comments" + autoload :Errors, "prism/parse_result/errors" + autoload :Newlines, "prism/parse_result/newlines" + + private_constant :Comments + private_constant :Errors + private_constant :Newlines + # The syntax tree that was parsed from the source code. attr_reader :value @@ -587,6 +604,23 @@ module Prism def deconstruct_keys(keys) super.merge!(value: value) end + + # Attach the list of comments to their respective locations in the tree. + def attach_comments! + Comments.new(self).attach! # steep:ignore + end + + # Walk the tree and mark nodes that are on a new line, loosely emulating + # the behavior of CRuby's `:line` tracepoint event. + def mark_newlines! + value.accept(Newlines.new(source.offsets.size)) # steep:ignore + end + + # Returns a string representation of the syntax tree with the errors + # displayed inline. + def errors_format + Errors.new(self).format + end end # This is a result specific to the `lex` and `lex_file` methods. @@ -677,5 +711,11 @@ module Prism other.type == type && other.value == value end + + # Returns a string representation of this token. + def inspect + location + super + end end end diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb index 3fa0603d74..22c4148b2c 100644 --- a/lib/prism/parse_result/comments.rb +++ b/lib/prism/parse_result/comments.rb @@ -183,12 +183,5 @@ module Prism [preceding, NodeTarget.new(node), following] end end - - private_constant :Comments - - # Attach the list of comments to their respective locations in the tree. - def attach_comments! - Comments.new(self).attach! # steep:ignore - end end end diff --git a/lib/prism/parse_result/errors.rb b/lib/prism/parse_result/errors.rb new file mode 100644 index 0000000000..847a8442fe --- /dev/null +++ b/lib/prism/parse_result/errors.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "stringio" + +module Prism + class ParseResult < Result + # An object to represent the set of errors on a parse result. This object + # can be used to format the errors in a human-readable way. + class Errors + # The parse result that contains the errors. + attr_reader :parse_result + + # Initialize a new set of errors from the given parse result. + def initialize(parse_result) + @parse_result = parse_result + end + + # Formats the errors in a human-readable way and return them as a string. + def format + error_lines = {} + parse_result.errors.each do |error| + location = error.location + (location.start_line..location.end_line).each do |line| + error_lines[line] ||= [] + error_lines[line] << error + end + end + + source_lines = parse_result.source.source.lines + source_lines << "" if error_lines.key?(source_lines.size + 1) + + io = StringIO.new + source_lines.each.with_index(1) do |line, line_number| + io.puts(line) + + (error_lines.delete(line_number) || []).each do |error| + location = error.location + + case line_number + when location.start_line + io.print(" " * location.start_column + "^") + + if location.start_line == location.end_line + if location.start_column != location.end_column + io.print("~" * (location.end_column - location.start_column - 1)) + end + + io.puts(" " + error.message) + else + io.puts("~" * (line.bytesize - location.start_column)) + end + when location.end_line + io.puts("~" * location.end_column + " " + error.message) + else + io.puts("~" * line.bytesize) + end + end + end + + io.puts + io.string + end + end + end +end diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb index 4a8151cc09..a04fa78a75 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -17,21 +17,27 @@ module Prism # Note that the logic in this file should be kept in sync with the Java # MarkNewlinesVisitor, since that visitor is responsible for marking the # newlines for JRuby/TruffleRuby. + # + # This file is autoloaded only when `mark_newlines!` is called, so the + # re-opening of the various nodes in this file will only be performed in + # that case. We do that to avoid storing the extra `@newline` instance + # variable on every node if we don't need it. class Newlines < Visitor # Create a new Newlines visitor with the given newline offsets. - def initialize(newline_marked) - @newline_marked = newline_marked + def initialize(lines) + # @type var lines: Integer + @lines = Array.new(1 + lines, false) end # Permit block/lambda nodes to mark newlines within themselves. def visit_block_node(node) - old_newline_marked = @newline_marked - @newline_marked = Array.new(old_newline_marked.size, false) + old_lines = @lines + @lines = Array.new(old_lines.size, false) begin super(node) ensure - @newline_marked = old_newline_marked + @lines = old_lines end end @@ -39,7 +45,7 @@ module Prism # Mark if/unless nodes as newlines. def visit_if_node(node) - node.set_newline_flag(@newline_marked) + node.newline_flag!(@lines) super(node) end @@ -48,17 +54,101 @@ module Prism # Permit statements lists to mark newlines within themselves. def visit_statements_node(node) node.body.each do |child| - child.set_newline_flag(@newline_marked) + child.newline_flag!(@lines) end super(node) end end + end + + class Node + def newline_flag? # :nodoc: + @newline_flag ? true : false + end + + def newline_flag!(lines) # :nodoc: + line = location.start_line + unless lines[line] + lines[line] = true + @newline_flag = true + end + end + end + + class BeginNode < Node + def newline_flag!(lines) # :nodoc: + # Never mark BeginNode with a newline flag, mark children instead. + end + end + + class ParenthesesNode < Node + def newline_flag!(lines) # :nodoc: + # Never mark ParenthesesNode with a newline flag, mark children instead. + end + end + + class IfNode < Node + def newline_flag!(lines) # :nodoc: + predicate.newline_flag!(lines) + end + end + + class UnlessNode < Node + def newline_flag!(lines) # :nodoc: + predicate.newline_flag!(lines) + end + end + + class UntilNode < Node + def newline_flag!(lines) # :nodoc: + predicate.newline_flag!(lines) + end + end + + class WhileNode < Node + def newline_flag!(lines) # :nodoc: + predicate.newline_flag!(lines) + end + end + + class RescueModifierNode < Node + def newline_flag!(lines) # :nodoc: + expression.newline_flag!(lines) + end + end + + class InterpolatedMatchLastLineNode < Node + def newline_flag!(lines) # :nodoc: + first = parts.first + first.newline_flag!(lines) if first + end + end + + class InterpolatedRegularExpressionNode < Node + def newline_flag!(lines) # :nodoc: + first = parts.first + first.newline_flag!(lines) if first + end + end + + class InterpolatedStringNode < Node + def newline_flag!(lines) # :nodoc: + first = parts.first + first.newline_flag!(lines) if first + end + end - private_constant :Newlines + class InterpolatedSymbolNode < Node + def newline_flag!(lines) # :nodoc: + first = parts.first + first.newline_flag!(lines) if first + end + end - # Walk the tree and mark nodes that are on a new line. - def mark_newlines! - value.accept(Newlines.new(Array.new(1 + source.offsets.size, false))) # steep:ignore + class InterpolatedXStringNode < Node + def newline_flag!(lines) # :nodoc: + first = parts.first + first.newline_flag!(lines) if first end end end diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index 374591bb70..37aa979576 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = "prism" - spec.version = "0.29.0" + spec.version = "1.0.0" spec.authors = ["Shopify"] spec.email = ["ruby@shopify.com"] @@ -65,12 +65,10 @@ Gem::Specification.new do |spec| "include/prism/util/pm_newline_list.h", "include/prism/util/pm_strncasecmp.h", "include/prism/util/pm_string.h", - "include/prism/util/pm_string_list.h", "include/prism/util/pm_strpbrk.h", "include/prism/version.h", "lib/prism.rb", "lib/prism/compiler.rb", - "lib/prism/debug.rb", "lib/prism/desugar_compiler.rb", "lib/prism/dispatcher.rb", "lib/prism/dot_visitor.rb", @@ -84,6 +82,7 @@ Gem::Specification.new do |spec| "lib/prism/pack.rb", "lib/prism/parse_result.rb", "lib/prism/parse_result/comments.rb", + "lib/prism/parse_result/errors.rb", "lib/prism/parse_result/newlines.rb", "lib/prism/pattern.rb", "lib/prism/polyfill/byteindex.rb", @@ -96,7 +95,6 @@ Gem::Specification.new do |spec| "lib/prism/translation/parser34.rb", "lib/prism/translation/parser/compiler.rb", "lib/prism/translation/parser/lexer.rb", - "lib/prism/translation/parser/rubocop.rb", "lib/prism/translation/ripper.rb", "lib/prism/translation/ripper/sexp.rb", "lib/prism/translation/ripper/shim.rb", @@ -105,6 +103,7 @@ Gem::Specification.new do |spec| "prism.gemspec", "rbi/prism.rbi", "rbi/prism/compiler.rbi", + "rbi/prism/dsl.rbi", "rbi/prism/inspect_visitor.rbi", "rbi/prism/node_ext.rbi", "rbi/prism/node.rbi", @@ -149,7 +148,6 @@ Gem::Specification.new do |spec| "src/util/pm_list.c", "src/util/pm_memchr.c", "src/util/pm_newline_list.c", - "src/util/pm_string_list.c", "src/util/pm_string.c", "src/util/pm_strncasecmp.c", "src/util/pm_strpbrk.c" diff --git a/lib/prism/translation/parser.rb b/lib/prism/translation/parser.rb index 193bbae406..8c7eb3aa75 100644 --- a/lib/prism/translation/parser.rb +++ b/lib/prism/translation/parser.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require "parser" +begin + require "parser" +rescue LoadError + warn(%q{Error: Unable to load parser. Add `gem "parser"` to your Gemfile.}) + exit(1) +end module Prism module Translation @@ -46,7 +51,7 @@ module Prism source = source_buffer.source offset_cache = build_offset_cache(source) - result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache) + result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache) build_ast(result.value, offset_cache) ensure @@ -59,7 +64,7 @@ module Prism source = source_buffer.source offset_cache = build_offset_cache(source) - result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache) + result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache) [ build_ast(result.value, offset_cache), @@ -78,7 +83,7 @@ module Prism offset_cache = build_offset_cache(source) result = begin - unwrap(Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache) + unwrap(Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache) rescue ::Parser::SyntaxError raise if !recover end diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb index a6c3118efd..d57b5757d7 100644 --- a/lib/prism/translation/parser/compiler.rb +++ b/lib/prism/translation/parser/compiler.rb @@ -90,7 +90,11 @@ module Prism end if node.constant - builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc)) + if visited.empty? + builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc)) + else + builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc)) + end else builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)) end @@ -105,38 +109,46 @@ module Prism # { a: 1 } # ^^^^ def visit_assoc_node(node) + key = node.key + if in_pattern if node.value.is_a?(ImplicitNode) - if node.key.is_a?(SymbolNode) - builder.match_hash_var([node.key.unescaped, srange(node.key.location)]) + if key.is_a?(SymbolNode) + if key.opening.nil? + builder.match_hash_var([key.unescaped, srange(key.location)]) + else + builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc)) + end else - builder.match_hash_var_from_str(token(node.key.opening_loc), visit_all(node.key.parts), token(node.key.closing_loc)) + builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc)) end + elsif key.opening.nil? + builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value)) else - builder.pair_keyword([node.key.unescaped, srange(node.key.location)], visit(node.value)) + builder.pair_quoted(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc), visit(node.value)) end elsif node.value.is_a?(ImplicitNode) if (value = node.value.value).is_a?(LocalVariableReadNode) builder.pair_keyword( - [node.key.unescaped, srange(node.key)], - builder.ident([value.name, srange(node.key.value_loc)]).updated(:lvar) + [key.unescaped, srange(key)], + builder.ident([value.name, srange(key.value_loc)]).updated(:lvar) ) else - builder.pair_label([node.key.unescaped, srange(node.key.location)]) + builder.pair_label([key.unescaped, srange(key.location)]) end elsif node.operator_loc - builder.pair(visit(node.key), token(node.operator_loc), visit(node.value)) - elsif node.key.is_a?(SymbolNode) && node.key.opening_loc.nil? - builder.pair_keyword([node.key.unescaped, srange(node.key.location)], visit(node.value)) + builder.pair(visit(key), token(node.operator_loc), visit(node.value)) + elsif key.is_a?(SymbolNode) && key.opening_loc.nil? + builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value)) else parts = - if node.key.is_a?(SymbolNode) - [builder.string_internal([node.key.unescaped, srange(node.key.value_loc)])] + if key.is_a?(SymbolNode) + [builder.string_internal([key.unescaped, srange(key.value_loc)])] else - visit_all(node.key.parts) + visit_all(key.parts) end - builder.pair_quoted(token(node.key.opening_loc), parts, token(node.key.closing_loc), visit(node.value)) + builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value)) end end @@ -146,7 +158,9 @@ module Prism # { **foo } # ^^^^^ def visit_assoc_splat_node(node) - if node.value.nil? && forwarding.include?(:**) + if in_pattern + builder.match_rest(token(node.operator_loc), token(node.value&.location)) + elsif node.value.nil? && forwarding.include?(:**) builder.forwarded_kwrestarg(token(node.operator_loc)) else builder.kwsplat(token(node.operator_loc), visit(node.value)) @@ -167,7 +181,7 @@ module Prism if (rescue_clause = node.rescue_clause) begin find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset - find_end_offset = (rescue_clause.statements&.location&.start_offset || rescue_clause.consequent&.location&.start_offset || (find_start_offset + 1)) + find_end_offset = (rescue_clause.statements&.location&.start_offset || rescue_clause.subsequent&.location&.start_offset || (find_start_offset + 1)) rescue_bodies << builder.rescue_body( token(rescue_clause.keyword_loc), @@ -177,7 +191,7 @@ module Prism srange_find(find_start_offset, find_end_offset, [";"]), visit(rescue_clause.statements) ) - end until (rescue_clause = rescue_clause.consequent).nil? + end until (rescue_clause = rescue_clause.subsequent).nil? end begin_body = @@ -396,8 +410,8 @@ module Prism token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), - token(node.consequent&.else_keyword_loc), - visit(node.consequent), + token(node.else_clause&.else_keyword_loc), + visit(node.else_clause), token(node.end_keyword_loc) ) end @@ -409,8 +423,8 @@ module Prism token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), - token(node.consequent&.else_keyword_loc), - visit(node.consequent), + token(node.else_clause&.else_keyword_loc), + visit(node.else_clause), token(node.end_keyword_loc) ) end @@ -844,8 +858,8 @@ module Prism visit(node.predicate), token(node.then_keyword_loc), visit(node.statements), - token(node.consequent.else_keyword_loc), - visit(node.consequent) + token(node.subsequent.else_keyword_loc), + visit(node.subsequent) ) elsif node.if_keyword_loc.start_offset == node.location.start_offset builder.condition( @@ -854,16 +868,16 @@ module Prism if node.then_keyword_loc token(node.then_keyword_loc) else - srange_find(node.predicate.location.end_offset, (node.statements&.location || node.consequent&.location || node.end_keyword_loc).start_offset, [";"]) + srange_find(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset, [";"]) end, visit(node.statements), - case node.consequent + case node.subsequent when IfNode - token(node.consequent.if_keyword_loc) + token(node.subsequent.if_keyword_loc) when ElseNode - token(node.consequent.else_keyword_loc) + token(node.subsequent.else_keyword_loc) end, - visit(node.consequent), + visit(node.subsequent), if node.if_keyword != "elsif" token(node.end_keyword_loc) end @@ -871,7 +885,7 @@ module Prism else builder.condition_mod( visit(node.statements), - visit(node.consequent), + visit(node.subsequent), token(node.if_keyword_loc), visit(node.predicate) ) @@ -881,7 +895,7 @@ module Prism # 1i # ^^ def visit_imaginary_node(node) - visit_numeric(node, builder.complex([imaginary_value(node), srange(node.location)])) + visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)])) end # { foo: } @@ -1064,36 +1078,7 @@ module Prism # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) if node.heredoc? - children, closing = visit_heredoc(node) - opening = token(node.opening_loc) - - start_offset = node.opening_loc.end_offset + 1 - end_offset = node.parts.first.location.start_offset - - # In the below case, the offsets should be the same: - # - # <<~HEREDOC - # a #{b} - # HEREDOC - # - # But in this case, the end_offset would be greater than the start_offset: - # - # <<~HEREDOC - # #{b} - # HEREDOC - # - # So we need to make sure the result node's heredoc range is correct, without updating the children - result = if start_offset < end_offset - # We need to add a padding string to ensure that the heredoc has correct range for its body - padding_string_node = builder.string_internal(["", srange_offsets(start_offset, end_offset)]) - node_with_correct_location = builder.string_compose(opening, [padding_string_node, *children], closing) - # But the padding string should not be included in the final AST, so we need to update the result's children - node_with_correct_location.updated(:dstr, children) - else - builder.string_compose(opening, children, closing) - end - - return result + return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } end parts = if node.parts.one? { |part| part.type == :string_node } @@ -1137,8 +1122,7 @@ module Prism # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) if node.heredoc? - children, closing = visit_heredoc(node) - builder.xstring_compose(token(node.opening_loc), children, closing) + visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } else builder.xstring_compose( token(node.opening_loc), @@ -1149,6 +1133,12 @@ module Prism end # -> { it } + # ^^ + def visit_it_local_variable_read_node(node) + builder.ident([:it, srange(node.location)]).updated(:lvar) + end + + # -> { it } # ^^^^^^^^^ def visit_it_parameters_node(node) builder.args(nil, [], nil, false) @@ -1201,14 +1191,7 @@ module Prism # foo # ^^^ def visit_local_variable_read_node(node) - name = node.name - - # This is just a guess. parser doesn't have support for the implicit - # `it` variable yet, so we'll probably have to visit this once it - # does. - name = :it if name == :"0it" - - builder.ident([name, srange(node.location)]).updated(:lvar) + builder.ident([node.name, srange(node.location)]).updated(:lvar) end # foo = 1 @@ -1312,13 +1295,9 @@ module Prism # foo, bar = baz # ^^^^^^^^ def visit_multi_target_node(node) - elements = [*node.lefts] - elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) - elements.concat(node.rights) - builder.multi_lhs( token(node.lparen_loc), - visit_all(elements), + visit_all(multi_target_elements(node)), token(node.rparen_loc) ) end @@ -1326,9 +1305,11 @@ module Prism # foo, bar = baz # ^^^^^^^^^^^^^^ def visit_multi_write_node(node) - elements = [*node.lefts] - elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) - elements.concat(node.rights) + elements = multi_target_elements(node) + + if elements.length == 1 && elements.first.is_a?(MultiTargetNode) + elements = multi_target_elements(elements.first) + end builder.multi_assign( builder.multi_lhs( @@ -1409,12 +1390,12 @@ module Prism if node.requireds.any? node.requireds.each do |required| - if required.is_a?(RequiredParameterNode) - params << visit(required) - else - compiler = copy_compiler(in_destructure: true) - params << required.accept(compiler) - end + params << + if required.is_a?(RequiredParameterNode) + visit(required) + else + required.accept(copy_compiler(in_destructure: true)) + end end end @@ -1423,12 +1404,12 @@ module Prism if node.posts.any? node.posts.each do |post| - if post.is_a?(RequiredParameterNode) - params << visit(post) - else - compiler = copy_compiler(in_destructure: true) - params << post.accept(compiler) - end + params << + if post.is_a?(RequiredParameterNode) + visit(post) + else + post.accept(copy_compiler(in_destructure: true)) + end end end @@ -1514,7 +1495,7 @@ module Prism # 1r # ^^ def visit_rational_node(node) - visit_numeric(node, builder.rational([rational_value(node), srange(node.location)])) + visit_numeric(node, builder.rational([node.value, srange(node.location)])) end # redo @@ -1526,9 +1507,20 @@ module Prism # /foo/ # ^^^^^ def visit_regular_expression_node(node) + content = node.content + parts = + if content.include?("\n") + offset = node.content_loc.start_offset + content.lines.map do |line| + builder.string_internal([line, srange_offsets(offset, offset += line.bytesize)]) + end + else + [builder.string_internal(token(node.content_loc))] + end + builder.regexp_compose( token(node.opening_loc), - [builder.string_internal(token(node.content_loc))], + parts, [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)], builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)]) ) @@ -1674,10 +1666,11 @@ module Prism # ^^^^^ def visit_string_node(node) if node.heredoc? - children, closing = visit_heredoc(node.to_interpolated) - builder.string_compose(token(node.opening_loc), children, closing) + visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } elsif node.opening == "?" builder.character([node.unescaped, srange(node.location)]) + elsif node.opening&.start_with?("%") && node.unescaped.empty? + builder.string_compose(token(node.opening_loc), [], token(node.closing_loc)) else content_lines = node.content.lines unescaped_lines = node.unescaped.lines @@ -1791,16 +1784,16 @@ module Prism if node.then_keyword_loc token(node.then_keyword_loc) else - srange_find(node.predicate.location.end_offset, (node.statements&.location || node.consequent&.location || node.end_keyword_loc).start_offset, [";"]) + srange_find(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset, [";"]) end, - visit(node.consequent), - token(node.consequent&.else_keyword_loc), + visit(node.else_clause), + token(node.else_clause&.else_keyword_loc), visit(node.statements), token(node.end_keyword_loc) ) else builder.condition_mod( - visit(node.consequent), + visit(node.else_clause), visit(node.statements), token(node.keyword_loc), visit(node.predicate) @@ -1877,8 +1870,7 @@ module Prism # ^^^^^ def visit_x_string_node(node) if node.heredoc? - children, closing = visit_heredoc(node.to_interpolated) - builder.xstring_compose(token(node.opening_loc), children, closing) + visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } else parts = if node.unescaped.lines.one? [builder.string_internal([node.unescaped, srange(node.content_loc)])] @@ -1940,10 +1932,12 @@ module Prism forwarding end - # Because we have mutated the AST to allow for newlines in the middle of - # a rational, we need to manually handle the value here. - def imaginary_value(node) - Complex(0, node.numeric.is_a?(RationalNode) ? rational_value(node.numeric) : node.numeric.value) + # Returns the set of targets for a MultiTargetNode or a MultiWriteNode. + def multi_target_elements(node) + elements = [*node.lefts] + elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) + elements.concat(node.rights) + elements end # Negate the value of a numeric node. This is a special case where you @@ -1955,7 +1949,9 @@ module Prism case receiver.type when :integer_node, :float_node receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location)) - when :rational_node, :imaginary_node + when :rational_node + receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location)) + when :imaginary_node receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location)) end end @@ -1974,16 +1970,6 @@ module Prism parameters.block.nil? end - # Because we have mutated the AST to allow for newlines in the middle of - # a rational, we need to manually handle the value here. - def rational_value(node) - if node.numeric.is_a?(IntegerNode) - Rational(node.numeric.value) - else - Rational(node.slice.gsub(/\s/, "").chomp("r")) - end - end - # Locations in the parser gem AST are generated using this class. We # store a reference to its constant to make it slightly faster to look # up. @@ -2006,7 +1992,7 @@ module Prism # Note that end_offset is allowed to be nil, in which case this will # search until the end of the string. def srange_find(start_offset, end_offset, tokens) - if (match = source_buffer.source.byteslice(start_offset...end_offset).match(/(\s*)(#{tokens.join("|")})/)) + if (match = source_buffer.source.byteslice(start_offset...end_offset).match(/\A(\s*)(#{tokens.join("|")})/)) _, whitespace, token = *match token_offset = start_offset + whitespace.bytesize @@ -2037,7 +2023,8 @@ module Prism token(parameters.opening_loc), if procarg0?(parameters.parameters) parameter = parameters.parameters.requireds.first - [builder.procarg0(visit(parameter))].concat(visit_all(parameters.locals)) + visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true)) + [builder.procarg0(visited)].concat(visit_all(parameters.locals)) else visit(parameters) end, @@ -2053,29 +2040,55 @@ module Prism end end + # The parser gem automatically converts \r\n to \n, meaning our offsets + # need to be adjusted to always subtract 1 from the length. + def chomped_bytesize(line) + chomped = line.chomp + chomped.bytesize + (chomped == line ? 0 : 1) + end + # Visit a heredoc that can be either a string or an xstring. def visit_heredoc(node) children = Array.new + indented = false + + # If this is a dedenting heredoc, then we need to insert the opening + # content into the children as well. + if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode) + location = node.parts.first.location + location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize) + children << builder.string_internal(token(location)) + indented = true + end + node.parts.each do |part| pushing = if part.is_a?(StringNode) && part.unescaped.include?("\n") - unescaped = part.unescaped.lines(chomp: true) - escaped = part.content.lines(chomp: true) + unescaped = part.unescaped.lines + escaped = part.content.lines - escaped_lengths = - if node.opening.end_with?("'") - escaped.map { |line| line.bytesize + 1 } - else - escaped.chunk_while { |before, after| before.match?(/(?<!\\)\\$/) }.map { |line| line.join.bytesize + line.length } + escaped_lengths = [] + normalized_lengths = [] + + if node.opening.end_with?("'") + escaped.each do |line| + escaped_lengths << line.bytesize + normalized_lengths << chomped_bytesize(line) end + else + escaped + .chunk_while { |before, after| before.match?(/(?<!\\)\\\r?\n$/) } + .each do |lines| + escaped_lengths << lines.sum(&:bytesize) + normalized_lengths << lines.sum { |line| chomped_bytesize(line) } + end + end start_offset = part.location.start_offset - end_offset = nil - unescaped.zip(escaped_lengths).map do |unescaped_line, escaped_length| - end_offset = start_offset + (escaped_length || 0) - inner_part = builder.string_internal(["#{unescaped_line}\n", srange_offsets(start_offset, end_offset)]) - start_offset = end_offset + unescaped.map.with_index do |unescaped_line, index| + inner_part = builder.string_internal([unescaped_line, srange_offsets(start_offset, start_offset + normalized_lengths.fetch(index, 0))]) + start_offset += escaped_lengths.fetch(index, 0) inner_part end else @@ -2086,7 +2099,12 @@ module Prism if child.type == :str && child.children.last == "" # nothing elsif child.type == :str && children.last && children.last.type == :str && !children.last.children.first.end_with?("\n") - children.last.children.first << child.children.first + appendee = children[-1] + + location = appendee.loc + location = location.with_expression(location.expression.join(child.loc.expression)) + + children[-1] = appendee.updated(:str, [appendee.children.first << child.children.first], location: location) else children << child end @@ -2095,8 +2113,10 @@ module Prism closing = node.closing closing_t = [closing.chomp, srange_offsets(node.closing_loc.start_offset, node.closing_loc.end_offset - (closing[/\s+$/]&.length || 0))] + composed = yield children, closing_t - [children, closing_t] + composed = composed.updated(nil, children[1..-1]) if indented + composed end # Visit a numeric node and account for the optional sign. diff --git a/lib/prism/translation/parser/lexer.rb b/lib/prism/translation/parser/lexer.rb index 9d7caae0ba..db7dbb1c87 100644 --- a/lib/prism/translation/parser/lexer.rb +++ b/lib/prism/translation/parser/lexer.rb @@ -134,7 +134,7 @@ module Prism MINUS_GREATER: :tLAMBDA, NEWLINE: :tNL, NUMBERED_REFERENCE: :tNTH_REF, - PARENTHESIS_LEFT: :tLPAREN, + PARENTHESIS_LEFT: :tLPAREN2, PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG, PARENTHESIS_RIGHT: :tRPAREN, PERCENT: :tPERCENT, @@ -173,7 +173,7 @@ module Prism UMINUS_NUM: :tUNARY_NUM, UPLUS: :tUPLUS, USTAR: :tSTAR, - USTAR_STAR: :tPOW, + USTAR_STAR: :tDSTAR, WORDS_SEP: :tSPACE } @@ -187,7 +187,20 @@ module Prism EXPR_BEG = 0x1 # :nodoc: EXPR_LABEL = 0x400 # :nodoc: - private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL + # It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`. + # + # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned + # instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046 + LAMBDA_TOKEN_TYPES = [:kDO_LAMBDA, :tLAMBDA, :tLAMBEG] + + # The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem. + # The following token types are listed as those classified as `tLPAREN`. + LPAREN_CONVERSION_TOKEN_TYPES = [ + :kBREAK, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3, + :tEQL, :tLPAREN, :tLPAREN2, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS + ] + + private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LAMBDA_TOKEN_TYPES, :LPAREN_CONVERSION_TOKEN_TYPES # The Parser::Source::Buffer that the tokens were lexed from. attr_reader :source_buffer @@ -229,6 +242,13 @@ module Prism location = Range.new(source_buffer, offset_cache[token.location.start_offset], offset_cache[token.location.end_offset]) case type + when :kDO + types = tokens.map(&:first) + nearest_lambda_token_type = types.reverse.find { |type| LAMBDA_TOKEN_TYPES.include?(type) } + + if nearest_lambda_token_type == :tLAMBDA + type = :kDO_LAMBDA + end when :tCHARACTER value.delete_prefix!("?") when :tCOMMENT @@ -268,6 +288,8 @@ module Prism value.chomp!(":") when :tLCURLY type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL + when :tLPAREN2 + type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0)) when :tNTH_REF value = parse_integer(value.delete_prefix("$")) when :tOP_ASGN @@ -339,7 +361,7 @@ module Prism location = Range.new(source_buffer, offset_cache[token.location.start_offset], offset_cache[token.location.start_offset + 1]) end when :tSYMBEG - if (next_token = lexed[index][0]) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR + if (next_token = lexed[index][0]) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR && next_token.type != :STRING_END next_location = token.location.join(next_token.location) type = :tSYMBOL value = next_token.value diff --git a/lib/prism/translation/parser/rubocop.rb b/lib/prism/translation/parser/rubocop.rb deleted file mode 100644 index 6c9687a5cc..0000000000 --- a/lib/prism/translation/parser/rubocop.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true -# typed: ignore - -warn "WARN: Prism is directly supported since RuboCop 1.62. The `prism/translation/parser/rubocop` file is deprecated." - -require "parser" -require "rubocop" - -require_relative "../../prism" -require_relative "../parser" - -module Prism - module Translation - class Parser - # This is the special version numbers that should be used in RuboCop - # configuration files to trigger using prism. - - # For Ruby 3.3 - VERSION_3_3 = 80_82_73_83_77.33 - - # For Ruby 3.4 - VERSION_3_4 = 80_82_73_83_77.34 - - # This module gets prepended into RuboCop::AST::ProcessedSource. - module ProcessedSource - # This condition is compatible with rubocop-ast versions up to 1.30.0. - if RuboCop::AST::ProcessedSource.instance_method(:parser_class).arity == 1 - # Redefine parser_class so that we can inject the prism parser into the - # list of known parsers. - def parser_class(ruby_version) - if ruby_version == Prism::Translation::Parser::VERSION_3_3 - warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.33` is deprecated. " \ - "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.3` instead." - require_relative "../parser33" - Prism::Translation::Parser33 - elsif ruby_version == Prism::Translation::Parser::VERSION_3_4 - warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.34` is deprecated. " \ - "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.4` instead." - require_relative "../parser34" - Prism::Translation::Parser34 - else - super - end - end - else - # Redefine parser_class so that we can inject the prism parser into the - # list of known parsers. - def parser_class(ruby_version, _parser_engine) - if ruby_version == Prism::Translation::Parser::VERSION_3_3 - warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.33` is deprecated. " \ - "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.3` instead." - require_relative "../parser33" - Prism::Translation::Parser33 - elsif ruby_version == Prism::Translation::Parser::VERSION_3_4 - warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.34` is deprecated. " \ - "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.4` instead." - require_relative "../parser34" - Prism::Translation::Parser34 - else - super - end - end - end - end - end - end -end - -# :stopdoc: -RuboCop::AST::ProcessedSource.prepend(Prism::Translation::Parser::ProcessedSource) -known_rubies = RuboCop::TargetRuby.const_get(:KNOWN_RUBIES) -RuboCop::TargetRuby.send(:remove_const, :KNOWN_RUBIES) -RuboCop::TargetRuby::KNOWN_RUBIES = [*known_rubies, Prism::Translation::Parser::VERSION_3_3].freeze diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 68f658565d..cafe8c3f63 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -1273,8 +1273,8 @@ module Prism def visit_case_node(node) predicate = visit(node.predicate) clauses = - node.conditions.reverse_each.inject(visit(node.consequent)) do |consequent, condition| - on_when(*visit(condition), consequent) + node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition| + on_when(*visit(condition), current) end bounds(node.location) @@ -1286,8 +1286,8 @@ module Prism def visit_case_match_node(node) predicate = visit(node.predicate) clauses = - node.conditions.reverse_each.inject(visit(node.consequent)) do |consequent, condition| - on_in(*visit(condition), consequent) + node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition| + on_in(*visit(condition), current) end bounds(node.location) @@ -1908,7 +1908,7 @@ module Prism if node.then_keyword == "?" predicate = visit(node.predicate) truthy = visit(node.statements.body.first) - falsy = visit(node.consequent.statements.body.first) + falsy = visit(node.subsequent.statements.body.first) bounds(node.location) on_ifop(predicate, truthy, falsy) @@ -1921,13 +1921,13 @@ module Prism else visit(node.statements) end - consequent = visit(node.consequent) + subsequent = visit(node.subsequent) bounds(node.location) if node.if_keyword == "if" - on_if(predicate, statements, consequent) + on_if(predicate, statements, subsequent) else - on_elsif(predicate, statements, consequent) + on_elsif(predicate, statements, subsequent) end else statements = visit(node.statements.body.first) @@ -1960,7 +1960,7 @@ module Prism # ^^^^^^^^^^^^^^^^^^^^^ def visit_in_node(node) # This is a special case where we're not going to call on_in directly - # because we don't have access to the consequent. Instead, we'll return + # because we don't have access to the subsequent. Instead, we'll return # the component parts and let the parent node handle it. pattern = visit_pattern_node(node.pattern) statements = @@ -2218,6 +2218,13 @@ module Prism end # -> { it } + # ^^ + def visit_it_local_variable_read_node(node) + bounds(node.location) + on_vcall(on_ident(node.slice)) + end + + # -> { it } # ^^^^^^^^^ def visit_it_parameters_node(node) end @@ -2312,12 +2319,7 @@ module Prism # ^^^ def visit_local_variable_read_node(node) bounds(node.location) - - if node.name == :"0it" - on_vcall(on_ident(node.slice)) - else - on_var_ref(on_ident(node.slice)) - end + on_var_ref(on_ident(node.slice)) end # foo = 1 @@ -2806,10 +2808,10 @@ module Prism visit(node.statements) end - consequent = visit(node.consequent) + subsequent = visit(node.subsequent) bounds(node.location) - on_rescue(exceptions, reference, statements, consequent) + on_rescue(exceptions, reference, statements, subsequent) end # def foo(*bar); end @@ -3130,10 +3132,10 @@ module Prism else visit(node.statements) end - consequent = visit(node.consequent) + else_clause = visit(node.else_clause) bounds(node.location) - on_unless(predicate, statements, consequent) + on_unless(predicate, statements, else_clause) else statements = visit(node.statements.body.first) predicate = visit(node.predicate) @@ -3174,7 +3176,7 @@ module Prism # ^^^^^^^^^^^^^ def visit_when_node(node) # This is a special case where we're not going to call on_when directly - # because we don't have access to the consequent. Instead, we'll return + # because we don't have access to the subsequent. Instead, we'll return # the component parts and let the parent node handle it. conditions = visit_arguments(node.conditions) statements = diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index d01a762a03..4ccff0b600 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require "ruby_parser" +begin + require "ruby_parser" +rescue LoadError + warn(%q{Error: Unable to load ruby_parser. Add `gem "ruby_parser"` to your Gemfile.}) + exit(1) +end module Prism module Translation @@ -50,7 +55,19 @@ module Prism # a and b # ^^^^^^^ def visit_and_node(node) - s(node, :and, visit(node.left), visit(node.right)) + left = visit(node.left) + + if left[0] == :and + # ruby_parser has the and keyword as right-associative as opposed to + # prism which has it as left-associative. We reverse that + # associativity here. + nest = left + nest = nest[2] while nest[2][0] == :and + nest[2] = s(node, :and, nest[2], visit(node.right)) + left + else + s(node, :and, left, visit(node.right)) + end end # [] @@ -130,7 +147,7 @@ module Prism end current = node.rescue_clause - until (current = current.consequent).nil? + until (current = current.subsequent).nil? result << visit(current) end end @@ -246,6 +263,11 @@ module Prism when RegularExpressionNode, InterpolatedRegularExpressionNode return s(node, :match2, visit(node.receiver), visit(node.arguments.arguments.first)) end + + case node.arguments.arguments.first + when RegularExpressionNode, InterpolatedRegularExpressionNode + return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver)) + end end end @@ -325,13 +347,13 @@ module Prism # case foo; when bar; end # ^^^^^^^^^^^^^^^^^^^^^^^ def visit_case_node(node) - s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.consequent) + s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause) end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_case_match_node(node) - s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.consequent) + s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause) end # class Foo; end @@ -480,9 +502,9 @@ module Prism def visit_constant_path_target_node(node) inner = if node.parent.nil? - s(node, :colon3, node.child.name) + s(node, :colon3, node.name) else - s(node, :colon2, visit(node.parent), node.child.name) + s(node, :colon2, visit(node.parent), node.name) end s(node, :const, inner) @@ -678,7 +700,7 @@ module Prism # foo ? bar : baz # ^^^^^^^^^^^^^^^ def visit_if_node(node) - s(node, :if, visit(node.predicate), visit(node.statements), visit(node.consequent)) + s(node, :if, visit(node.predicate), visit(node.statements), visit(node.subsequent)) end # 1i @@ -870,6 +892,15 @@ module Prism else visited << result end + elsif result[0] == :dstr + if !visited.empty? && part.parts[0].is_a?(StringNode) + # If we are in the middle of an implicitly concatenated string, + # we should not have a bare string as the first part. In this + # case we need to visit just that first part and then we can + # push the rest of the parts onto the visited array. + result[1] = visit(part.parts[0]) + end + visited.concat(result[1..-1]) else visited << result end @@ -900,12 +931,23 @@ module Prism results << result state = :interpolated_content end - else - results << result + when :interpolated_content + if result.is_a?(Array) && result[0] == :str && results[-1][0] == :str && (results[-1].line_max == result.line) + results[-1][1] << result[1] + results[-1].line_max = result.line_max + else + results << result + end end end end + # -> { it } + # ^^ + def visit_it_local_variable_read_node(node) + s(node, :call, nil, :it) + end + # foo(bar: baz) # ^^^^^^^^ def visit_keyword_hash_node(node) @@ -1118,7 +1160,19 @@ module Prism # a or b # ^^^^^^ def visit_or_node(node) - s(node, :or, visit(node.left), visit(node.right)) + left = visit(node.left) + + if left[0] == :or + # ruby_parser has the or keyword as right-associative as opposed to + # prism which has it as left-associative. We reverse that + # associativity here. + nest = left + nest = nest[2] while nest[2][0] == :or + nest[2] = s(node, :or, nest[2], visit(node.right)) + left + else + s(node, :or, left, visit(node.right)) + end end # def foo(bar, *baz); end @@ -1374,7 +1428,13 @@ module Prism # "foo" # ^^^^^ def visit_string_node(node) - s(node, :str, node.unescaped) + unescaped = node.unescaped + + if node.forced_binary_encoding? + unescaped.force_encoding(Encoding::BINARY) + end + + s(node, :str, unescaped) end # super(foo) @@ -1416,7 +1476,7 @@ module Prism # bar unless foo # ^^^^^^^^^^^^^^ def visit_unless_node(node) - s(node, :if, visit(node.predicate), visit(node.consequent), visit(node.statements)) + s(node, :if, visit(node.predicate), visit(node.else_clause), visit(node.statements)) end # until foo; bar end diff --git a/lib/random/formatter.rb b/lib/random/formatter.rb index 037f9d8748..2b5cf718ad 100644 --- a/lib/random/formatter.rb +++ b/lib/random/formatter.rb @@ -165,7 +165,7 @@ module Random::Formatter # # The result contains 122 random bits (15.25 random bytes). # - # See RFC4122[https://www.rfc-editor.org/rfc/rfc4122] for details of UUID. + # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv4. # def uuid ary = random_bytes(16) @@ -204,8 +204,7 @@ module Random::Formatter # Note that this method cannot be made reproducible because its output # includes not only random bits but also timestamp. # - # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/] - # for details of UUIDv7. + # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv7. # # ==== Monotonicity # @@ -242,7 +241,7 @@ module Random::Formatter # # Counters and other mechanisms for stronger guarantees of monotonicity are # not implemented. Applications with stricter requirements should follow - # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters] + # {Section 6.2}[https://www.rfc-editor.org/rfc/rfc9562.html#name-monotonicity-and-counters] # of the specification. # def uuid_v7(extra_timestamp_bits: 0) diff --git a/lib/rdoc.rb b/lib/rdoc.rb index 9dc4595324..3821569f45 100644 --- a/lib/rdoc.rb +++ b/lib/rdoc.rb @@ -21,7 +21,7 @@ $DEBUG_RDOC = nil # see RDoc::Markup and refer to <tt>rdoc --help</tt> for command line usage. # # If you want to set the default markup format see -# RDoc::Markup@Supported+Formats +# RDoc::Markup@Markup+Formats # # If you want to store rdoc configuration in your gem (such as the default # markup format) see RDoc::Options@Saved+Options @@ -188,26 +188,26 @@ module RDoc # programs: classes, modules, methods, and so on. autoload :CodeObject, "#{__dir__}/rdoc/code_object" - autoload :Context, "#{__dir__}/rdoc/context" - autoload :TopLevel, "#{__dir__}/rdoc/top_level" - - autoload :AnonClass, "#{__dir__}/rdoc/anon_class" - autoload :ClassModule, "#{__dir__}/rdoc/class_module" - autoload :NormalClass, "#{__dir__}/rdoc/normal_class" - autoload :NormalModule, "#{__dir__}/rdoc/normal_module" - autoload :SingleClass, "#{__dir__}/rdoc/single_class" - - autoload :Alias, "#{__dir__}/rdoc/alias" - autoload :AnyMethod, "#{__dir__}/rdoc/any_method" - autoload :MethodAttr, "#{__dir__}/rdoc/method_attr" - autoload :GhostMethod, "#{__dir__}/rdoc/ghost_method" - autoload :MetaMethod, "#{__dir__}/rdoc/meta_method" - autoload :Attr, "#{__dir__}/rdoc/attr" - - autoload :Constant, "#{__dir__}/rdoc/constant" - autoload :Mixin, "#{__dir__}/rdoc/mixin" - autoload :Include, "#{__dir__}/rdoc/include" - autoload :Extend, "#{__dir__}/rdoc/extend" - autoload :Require, "#{__dir__}/rdoc/require" + autoload :Context, "#{__dir__}/rdoc/code_object/context" + autoload :TopLevel, "#{__dir__}/rdoc/code_object/top_level" + + autoload :AnonClass, "#{__dir__}/rdoc/code_object/anon_class" + autoload :ClassModule, "#{__dir__}/rdoc/code_object/class_module" + autoload :NormalClass, "#{__dir__}/rdoc/code_object/normal_class" + autoload :NormalModule, "#{__dir__}/rdoc/code_object/normal_module" + autoload :SingleClass, "#{__dir__}/rdoc/code_object/single_class" + + autoload :Alias, "#{__dir__}/rdoc/code_object/alias" + autoload :AnyMethod, "#{__dir__}/rdoc/code_object/any_method" + autoload :MethodAttr, "#{__dir__}/rdoc/code_object/method_attr" + autoload :GhostMethod, "#{__dir__}/rdoc/code_object/ghost_method" + autoload :MetaMethod, "#{__dir__}/rdoc/code_object/meta_method" + autoload :Attr, "#{__dir__}/rdoc/code_object/attr" + + autoload :Constant, "#{__dir__}/rdoc/code_object/constant" + autoload :Mixin, "#{__dir__}/rdoc/code_object/mixin" + autoload :Include, "#{__dir__}/rdoc/code_object/include" + autoload :Extend, "#{__dir__}/rdoc/code_object/extend" + autoload :Require, "#{__dir__}/rdoc/code_object/require" end diff --git a/lib/rdoc/alias.rb b/lib/rdoc/code_object/alias.rb index 446cf9ccb4..92df7e448f 100644 --- a/lib/rdoc/alias.rb +++ b/lib/rdoc/code_object/alias.rb @@ -70,7 +70,7 @@ class RDoc::Alias < RDoc::CodeObject # HTML id-friendly version of +#new_name+. def html_name - CGI.escape(@new_name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + CGI.escape(@new_name.gsub('-', '-2D')).gsub('%', '-').sub(/^-/, '') end def inspect # :nodoc: diff --git a/lib/rdoc/anon_class.rb b/lib/rdoc/code_object/anon_class.rb index 3c2f0e1877..3c2f0e1877 100644 --- a/lib/rdoc/anon_class.rb +++ b/lib/rdoc/code_object/anon_class.rb diff --git a/lib/rdoc/any_method.rb b/lib/rdoc/code_object/any_method.rb index 465c4a4fb2..465c4a4fb2 100644 --- a/lib/rdoc/any_method.rb +++ b/lib/rdoc/code_object/any_method.rb diff --git a/lib/rdoc/attr.rb b/lib/rdoc/code_object/attr.rb index a403235933..a403235933 100644 --- a/lib/rdoc/attr.rb +++ b/lib/rdoc/code_object/attr.rb diff --git a/lib/rdoc/class_module.rb b/lib/rdoc/code_object/class_module.rb index c69e14b5e4..c69e14b5e4 100644 --- a/lib/rdoc/class_module.rb +++ b/lib/rdoc/code_object/class_module.rb diff --git a/lib/rdoc/constant.rb b/lib/rdoc/code_object/constant.rb index 12b8be775c..12b8be775c 100644 --- a/lib/rdoc/constant.rb +++ b/lib/rdoc/code_object/constant.rb diff --git a/lib/rdoc/context.rb b/lib/rdoc/code_object/context.rb index c688d562c3..c688d562c3 100644 --- a/lib/rdoc/context.rb +++ b/lib/rdoc/code_object/context.rb diff --git a/lib/rdoc/context/section.rb b/lib/rdoc/code_object/context/section.rb index aecd4e0213..aecd4e0213 100644 --- a/lib/rdoc/context/section.rb +++ b/lib/rdoc/code_object/context/section.rb diff --git a/lib/rdoc/extend.rb b/lib/rdoc/code_object/extend.rb index 7d57433de6..7d57433de6 100644 --- a/lib/rdoc/extend.rb +++ b/lib/rdoc/code_object/extend.rb diff --git a/lib/rdoc/ghost_method.rb b/lib/rdoc/code_object/ghost_method.rb index 25f951e35e..25f951e35e 100644 --- a/lib/rdoc/ghost_method.rb +++ b/lib/rdoc/code_object/ghost_method.rb diff --git a/lib/rdoc/include.rb b/lib/rdoc/code_object/include.rb index c3e0d45e47..c3e0d45e47 100644 --- a/lib/rdoc/include.rb +++ b/lib/rdoc/code_object/include.rb diff --git a/lib/rdoc/meta_method.rb b/lib/rdoc/code_object/meta_method.rb index 8c95a0f78c..8c95a0f78c 100644 --- a/lib/rdoc/meta_method.rb +++ b/lib/rdoc/code_object/meta_method.rb diff --git a/lib/rdoc/method_attr.rb b/lib/rdoc/code_object/method_attr.rb index 61ddb32f46..27e6599bc1 100644 --- a/lib/rdoc/method_attr.rb +++ b/lib/rdoc/code_object/method_attr.rb @@ -268,8 +268,8 @@ class RDoc::MethodAttr < RDoc::CodeObject when 'const_get' then 'const' when 'new' then $1.split('::').last. # ClassName => class_name - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). + gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). + gsub(/([a-z\d])([A-Z])/, '\1_\2'). downcase else $2 @@ -291,7 +291,7 @@ class RDoc::MethodAttr < RDoc::CodeObject def html_name require 'cgi/util' - CGI.escape(@name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + CGI.escape(@name.gsub('-', '-2D')).gsub('%', '-').sub(/^-/, '') end ## diff --git a/lib/rdoc/mixin.rb b/lib/rdoc/code_object/mixin.rb index fa8faefc15..fa8faefc15 100644 --- a/lib/rdoc/mixin.rb +++ b/lib/rdoc/code_object/mixin.rb diff --git a/lib/rdoc/normal_class.rb b/lib/rdoc/code_object/normal_class.rb index aa340b5d15..aa340b5d15 100644 --- a/lib/rdoc/normal_class.rb +++ b/lib/rdoc/code_object/normal_class.rb diff --git a/lib/rdoc/normal_module.rb b/lib/rdoc/code_object/normal_module.rb index 498ec4dde2..498ec4dde2 100644 --- a/lib/rdoc/normal_module.rb +++ b/lib/rdoc/code_object/normal_module.rb diff --git a/lib/rdoc/require.rb b/lib/rdoc/code_object/require.rb index 05e26b84b0..05e26b84b0 100644 --- a/lib/rdoc/require.rb +++ b/lib/rdoc/code_object/require.rb diff --git a/lib/rdoc/single_class.rb b/lib/rdoc/code_object/single_class.rb index dd16529648..dd16529648 100644 --- a/lib/rdoc/single_class.rb +++ b/lib/rdoc/code_object/single_class.rb diff --git a/lib/rdoc/top_level.rb b/lib/rdoc/code_object/top_level.rb index 3864f66431..b93e3802fc 100644 --- a/lib/rdoc/top_level.rb +++ b/lib/rdoc/code_object/top_level.rb @@ -183,8 +183,8 @@ class RDoc::TopLevel < RDoc::Context "#<%s:0x%x %p modules: %p classes: %p>" % [ self.class, object_id, base_name, - @modules.map { |n,m| m }, - @classes.map { |n,c| c } + @modules.map { |n, m| m }, + @classes.map { |n, c| c } ] end @@ -254,8 +254,8 @@ class RDoc::TopLevel < RDoc::Context q.text "base name: #{base_name.inspect}" q.breakable - items = @modules.map { |n,m| m } - items.concat @modules.map { |n,c| c } + items = @modules.map { |n, m| m } + items.concat @modules.map { |n, c| c } q.seplist items do |mod| q.pp mod end end end diff --git a/lib/rdoc/generator/darkfish.rb b/lib/rdoc/generator/darkfish.rb index 1b408a6f8e..96bb4fb66f 100644 --- a/lib/rdoc/generator/darkfish.rb +++ b/lib/rdoc/generator/darkfish.rb @@ -677,7 +677,6 @@ class RDoc::Generator::Darkfish return body if body =~ /<html/ head_file = @template_dir + '_head.rhtml' - footer_file = @template_dir + '_footer.rhtml' <<-TEMPLATE <!DOCTYPE html> @@ -687,8 +686,6 @@ class RDoc::Generator::Darkfish #{head_file.read} #{body} - -#{footer_file.read} TEMPLATE end diff --git a/lib/rdoc/generator/pot/message_extractor.rb b/lib/rdoc/generator/pot/message_extractor.rb index 313dfd2dc7..4938858bdc 100644 --- a/lib/rdoc/generator/pot/message_extractor.rb +++ b/lib/rdoc/generator/pot/message_extractor.rb @@ -29,7 +29,7 @@ class RDoc::Generator::POT::MessageExtractor extract_text(klass.comment_location, klass.full_name) klass.each_section do |section, constants, attributes| - extract_text(section.title ,"#{klass.full_name}: section title") + extract_text(section.title, "#{klass.full_name}: section title") section.comments.each do |comment| extract_text(comment, "#{klass.full_name}: #{section.title}") end diff --git a/lib/rdoc/generator/pot/po_entry.rb b/lib/rdoc/generator/pot/po_entry.rb index 3c278826f4..7454b29273 100644 --- a/lib/rdoc/generator/pot/po_entry.rb +++ b/lib/rdoc/generator/pot/po_entry.rb @@ -23,7 +23,7 @@ class RDoc::Generator::POT::POEntry attr_reader :flags ## - # Creates a PO entry for +msgid+. Other valus can be specified by + # Creates a PO entry for +msgid+. Other values can be specified by # +options+. def initialize msgid, options = {} diff --git a/lib/rdoc/generator/template/darkfish/_head.rhtml b/lib/rdoc/generator/template/darkfish/_head.rhtml index d5aed3e9ef..69649ad3b5 100644 --- a/lib/rdoc/generator/template/darkfish/_head.rhtml +++ b/lib/rdoc/generator/template/darkfish/_head.rhtml @@ -1,4 +1,5 @@ <meta charset="<%= @options.charset %>"> +<meta name="viewport" content="width=device-width, initial-scale=1" /> <title><%= h @title %></title> diff --git a/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml b/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml new file mode 100644 index 0000000000..ed2cbe31a0 --- /dev/null +++ b/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml @@ -0,0 +1,3 @@ +<div id="navigation-toggle" role="button" tabindex="0" aria-label="Toggle sidebar" aria-expanded="true" aria-controls="navigation"> + <span aria-hidden="true">☰</span> +</div> diff --git a/lib/rdoc/generator/template/darkfish/class.rhtml b/lib/rdoc/generator/template/darkfish/class.rhtml index d6510336df..6c64ba6c98 100644 --- a/lib/rdoc/generator/template/darkfish/class.rhtml +++ b/lib/rdoc/generator/template/darkfish/class.rhtml @@ -1,19 +1,20 @@ <body id="top" role="document" class="<%= klass.type %>"> -<nav role="navigation"> +<%= render '_sidebar_toggle.rhtml' %> + +<nav id="navigation" role="navigation"> <div id="project-navigation"> <%= render '_sidebar_navigation.rhtml' %> <%= render '_sidebar_search.rhtml' %> </div> <%= render '_sidebar_table_of_contents.rhtml' %> + <%= render '_sidebar_sections.rhtml' %> + <%= render '_sidebar_parent.rhtml' %> + <%= render '_sidebar_includes.rhtml' %> + <%= render '_sidebar_extends.rhtml' %> + <%= render '_sidebar_methods.rhtml' %> - <div id="class-metadata"> - <%= render '_sidebar_sections.rhtml' %> - <%= render '_sidebar_parent.rhtml' %> - <%= render '_sidebar_includes.rhtml' %> - <%= render '_sidebar_extends.rhtml' %> - <%= render '_sidebar_methods.rhtml' %> - </div> + <%= render '_footer.rhtml' %> </nav> <main role="main" aria-labelledby="<%=h klass.aref %>"> diff --git a/lib/rdoc/generator/template/darkfish/css/rdoc.css b/lib/rdoc/generator/template/darkfish/css/rdoc.css index 2cc55e03b1..169a6331e9 100644 --- a/lib/rdoc/generator/template/darkfish/css/rdoc.css +++ b/lib/rdoc/generator/template/darkfish/css/rdoc.css @@ -9,24 +9,42 @@ /* vim: ft=css et sw=2 ts=2 sts=2 */ /* Base Green is: #6C8C22 */ -.hide { display: none !important; } - -* { padding: 0; margin: 0; } - +/* 1. Variables and Root Styles */ +:root { + --sidebar-width: 300px; + --primary-color: #2c8c5e; /* A more muted green */ + --secondary-color: #246b4b; /* A darker, muted green */ + --text-color: #333; + --background-color: #f8f9fa; + --code-block-background-color: #f0f4f8; + --code-block-border-color: #d1d5da; + --link-color: #246b4b; /* A muted teal-green color */ + --link-hover-color: #25a28a; /* A slightly brighter teal-green */ + --border-color: #e0e0e0; + --sidebar-text-color: #2c3e50; /* Dark blue-gray for contrast */ + + /* Font family variables */ + --font-primary: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; + --font-heading: 'Helvetica Neue', Arial, sans-serif; + --font-code: 'Source Code Pro', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; +} + +/* 2. Global Styles */ body { - background: #fafafa; - font-family: Lato, sans-serif; - font-weight: 300; + background: var(--background-color); + font-family: var(--font-primary); + font-weight: 400; + color: var(--text-color); + line-height: 1.6; /* Layout */ - display: grid; - grid-template-columns: auto 1fr; -} - -body > :last-child { - grid-column: 1 / 3; + display: flex; + flex-direction: column; + min-height: 100vh; + margin: 0; } +/* 3. Typography */ h1 span, h2 span, h3 span, @@ -68,36 +86,36 @@ h6:target { border-left: 10px solid #f1edba; } +/* 4. Links */ :link, :visited { - color: #6C8C22; + color: var(--link-color); text-decoration: none; + transition: color 0.3s ease; + font-weight: 600; /* Make links bolder */ } :link:hover, :visited:hover { - border-bottom: 1px dotted #6C8C22; + color: var(--link-hover-color); + text-decoration: underline; } +/* 5. Code and Pre */ code, pre { - font-family: "Source Code Pro", Monaco, monospace; - background-color: rgba(27,31,35,0.05); - padding: 0em 0.2em; - border-radius: 0.2em; -} - -em { - text-decoration-color: rgba(52, 48, 64, 0.25); - text-decoration-line: underline; - text-decoration-style: dotted; -} - -strong, -em { - background-color: rgba(158, 178, 255, 0.1); -} - + font-family: var(--font-code); + background-color: var(--code-block-background-color); + border: 1px solid var(--code-block-border-color); + border-radius: 6px; + padding: 16px; + overflow-x: auto; + font-size: 15px; /* Increased from 14px */ + line-height: 1.5; /* Slightly increased for better readability with larger font */ + margin: 1em 0; /* Add some vertical margin */ +} + +/* 6. Tables */ table { margin: 0; border-spacing: 0; @@ -117,110 +135,142 @@ table tr:nth-child(even) td { background-color: #f5f4f6; } -/* @group Generic Classes */ - -.initially-hidden { +/* 7. Navigation and Sidebar */ +nav { + font-family: var(--font-heading); + font-size: 16px; + border-right: 1px solid var(--border-color); + position: fixed; + top: 0; + bottom: 0; + left: 0; + width: var(--sidebar-width); + background: var(--background-color); + overflow-y: auto; + z-index: 10; + display: flex; + flex-direction: column; + color: var(--sidebar-text-color); +} + +nav[hidden] { display: none; } -#search-field { - width: 98%; - background: white; - border: none; - height: 1.5em; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - text-align: left; +nav footer { + padding: 1em; + border-top: 1px solid #ccc; } -#search-field:focus { - background: #f1edba; + +nav .nav-section { + margin-top: 1em; + padding: 0 1em; } -#search-field:-moz-placeholder, -#search-field::-webkit-input-placeholder { - font-weight: bold; - color: #666; + +nav h2 { + margin: 0 0 0.5em; + padding: 0.5em 0; + font-size: 1.2em; + color: var(--text-color); + border-bottom: 1px solid var(--border-color); } -.missing-docs { - font-size: 120%; - background: white url(../images/wrench_orange.png) no-repeat 4px center; - color: #ccc; - line-height: 2em; - border: 1px solid #d00; - opacity: 1; - padding-left: 20px; - text-indent: 24px; - letter-spacing: 3px; - font-weight: bold; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; +nav h3, +#table-of-contents-navigation { + margin: 0; + padding: 0.5em 0; + font-size: 1em; + color: var(--text-color); } -.target-section { - border: 2px solid #dcce90; - border-left-width: 8px; - padding: 0 1em; - background: #fff3c2; +nav ul, +nav dl, +nav p { + padding: 0; + list-style: none; + margin: 0.5em 0; } -/* @end */ +nav ul li { + margin-bottom: 0.3em; +} -/* @group Index Page, Standalone file pages */ -.table-of-contents ul { - margin: 1em; - list-style: none; +nav ul ul { + padding-left: 1em; } -.table-of-contents ul ul { - margin-top: 0.25em; +nav ul ul ul { + padding-left: 1em; } -.table-of-contents ul :link, -.table-of-contents ul :visited { - font-size: 16px; +nav ul ul ul ul { + padding-left: 1em; } -.table-of-contents li { - margin-bottom: 0.25em; +nav a { + color: var(--link-color); + text-decoration: none; } -.table-of-contents li .toc-toggle { - width: 16px; - height: 16px; - background: url(../images/add.png) no-repeat; +nav a:hover { + text-decoration: underline; } -.table-of-contents li .toc-toggle.open { - background: url(../images/delete.png) no-repeat; +#navigation-toggle { + z-index: 1000; + font-size: 2em; + display: block; + position: fixed; + top: 10px; + left: 20px; + cursor: pointer; } -/* @end */ +#navigation-toggle[aria-expanded="true"] { + top: 10px; + left: 250px; +} -/* @group Top-Level Structure */ +nav ul li details { + position: relative; + padding-right: 1.5em; /* Add space for the marker on the right */ +} -nav { - font-family: Helvetica, sans-serif; - font-size: 14px; - border-right: 1px solid #ccc; - position: sticky; - top: 0; - overflow: auto; +nav ul li details > summary { + list-style: none; /* Remove the default marker */ + position: relative; /* So that the open/close triangle can position itself absolutely inside */ +} - /* Layout */ - width: 260px; /* fallback */ - width: max(50px, 20vw); - min-width: 50px; - max-width: 80vw; - height: calc(100vh - 100px); /* reduce the footer height */ - resize: horizontal; +nav ul li details > summary::after { + content: '▶'; /* Unicode right-pointing triangle */ + position: absolute; + font-size: 0.8em; + bottom: 0.1em; + margin-left: 0.3em; + transition: transform 0.2s ease; +} + +nav ul li details[open] > summary::after { + transform: rotate(90deg); /* Rotate the triangle when open */ } +/* 8. Main Content */ main { + flex: 1; display: block; - margin: 1em; - min-width: 340px; + margin: 3em auto; + padding: 0 2em; + max-width: 800px; font-size: 16px; + line-height: 1.6; + color: var(--text-color); + box-sizing: border-box; +} + +@media (min-width: 1024px) { + main { + margin-left: var(--sidebar-width); + } } main h1, @@ -229,11 +279,13 @@ main h3, main h4, main h5, main h6 { - font-family: Helvetica, sans-serif; + font-family: var(--font-heading); } -.table-of-contents main { - margin-left: 2em; +@media (min-width: 1024px) { + .table-of-contents main { + margin-left: 20em; + } } #validator-badges { @@ -241,121 +293,123 @@ main h6 { font-size: smaller; } -/* @end */ - -/* @group navigation */ -nav { - margin-bottom: 1em; +/* 9. Search */ +#search-section { + padding: 1em; + background-color: var(--background-color); + border-bottom: 1px solid var(--border-color); } -nav .nav-section { - margin-top: 2em; - border-top: 2px solid #aaa; - font-size: 90%; - overflow: hidden; +#search-field-wrapper { + position: relative; + display: flex; + align-items: center; } -nav h2 { - margin: 0; - padding: 2px 8px 2px 8px; - background-color: #e8e8e8; - color: #555; - font-size: 125%; - text-align: center; +#search-field { + width: 100%; + padding: 0.5em 1em 0.5em 2.5em; + border: 1px solid var(--border-color); + border-radius: 20px; + font-size: 14px; + outline: none; + transition: border-color 0.3s ease; + background-color: var(--background-color); } -nav h3, -#table-of-contents-navigation { - margin: 0; - padding: 2px 8px 2px 8px; - text-align: right; - background-color: #e8e8e8; - color: #555; +#search-field:focus { + border-color: var(--primary-color); } -nav ul, -nav dl, -nav p { - padding: 4px 8px 0; - list-style: none; +#search-field::placeholder { + color: var(--text-color); + opacity: 0.6; } -#project-navigation .nav-section { - margin: 0; - border-top: 0; +#search-field-wrapper::before { + content: "\1F50D"; /* Unicode for magnifying glass */ + position: absolute; + left: 0.75em; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + color: var(--text-color); + opacity: 0.6; } -#home-section h2 { - text-align: center; -} +/* 10. Utility Classes */ +.hide { display: none !important; } +.initially-hidden { display: none; } -#table-of-contents-navigation { - font-size: 1.2em; - font-weight: bold; - text-align: center; +/* 11. Media Queries */ +@media (min-width: 1024px) { + /* Styles for larger screens */ } -#search-section { - margin-top: 0; - border-top: 0; +/* 12. Print Styles */ +@media print { + /* Print-specific styles */ } -#search-field-wrapper { - border-top: 1px solid #aaa; - border-bottom: 1px solid #aaa; - padding: 3px 8px; - background-color: #e8e8e8; - color: #555; -} +/* 13. Syntax Highlighting */ +.ruby-constant { color: #0366d6; } /* Bright blue for constants */ +.ruby-keyword { color: #d73a49; } /* Red for keywords */ +.ruby-ivar { color: #e36209; } /* Orange for instance variables */ +.ruby-operator { color: #005cc5; } /* Dark blue for operators */ +.ruby-identifier { color: #24292e; } /* Dark gray for identifiers */ +.ruby-node { color: #22863a; } /* Green for interpolation */ +.ruby-comment { color: #6a737d; } /* Gray for comments */ +.ruby-regexp { color: #032f62; } /* Navy for regular expressions */ +.ruby-value { color: #005cc5; } /* Dark blue for numeric values */ +.ruby-string { color: #22863a; } /* Green for strings */ -ul.link-list li { - white-space: nowrap; - line-height: 1.4em; +code { + background-color: #f0f4f8; /* Match pre background */ + padding: 0.2em 0.4em; + border-radius: 3px; + font-size: 85%; } -ul.link-list .type { - font-size: 8px; - text-transform: uppercase; - color: white; - background: #969696; - padding: 2px 4px; - -webkit-border-radius: 5px; +em { + text-decoration-color: rgba(52, 48, 64, 0.25); + text-decoration-line: underline; + text-decoration-style: dotted; } -dl.note-list dt { - float: left; - margin-right: 1em; +strong, +em { + background-color: rgba(158, 178, 255, 0.1); } -.calls-super { - background: url(../images/arrow_up.png) no-repeat right center; +/* 14. Specific Component Styles */ +.table-of-contents ul { + margin: 1em; + list-style: none; } -.nav-section details > summary { - display: block; +.table-of-contents ul ul { + margin-top: 0.25em; } -.nav-section details > summary::-webkit-details-marker { - display: none; +.table-of-contents ul :link, +.table-of-contents ul :visited { + font-size: 16px; } -.nav-section details > summary::before { - content: ""; +.table-of-contents li { + margin-bottom: 0.25em; } -.nav-section details > summary::after { - content: "\25B6"; /* BLACK RIGHT-POINTING TRIANGLE */ - font-size: 0.8em; - margin-left: 0.4em; +.table-of-contents li .toc-toggle { + width: 16px; + height: 16px; + background: url(../images/add.png) no-repeat; } -.nav-section details[open] > summary::after { - content: "\25BD"; /* WHITE DOWN-POINTING TRIANGLE */ +.table-of-contents li .toc-toggle.open { + background: url(../images/delete.png) no-repeat; } -/* @end */ - -/* @group Documentation Section */ main { color: #333; } @@ -378,43 +432,43 @@ main sup { main h1[class] { margin-top: 0; margin-bottom: 1em; - font-size: 2em; - color: #6C8C22; + font-size: 2.5em; + color: var(--primary-color); } main h1 { - margin: 2em 0 0.5em; - font-size: 1.7em; + margin: 1.5em 0 0.5em; + font-size: 2.2em; + color: var(--secondary-color); } main h2 { - margin: 2em 0 0.5em; - font-size: 1.5em; + margin: 1.3em 0 0.5em; + font-size: 1.8em; + color: var(--secondary-color); } main h3 { - margin: 2em 0 0.5em; - font-size: 1.2em; + margin: 1.2em 0 0.5em; + font-size: 1.5em; + color: var(--secondary-color); } main h4 { - margin: 2em 0 0.5em; - font-size: 1.1em; -} - -main h5 { - margin: 2em 0 0.5em; - font-size: 1em; + margin: 1.1em 0 0.5em; + font-size: 1.3em; + color: var(--secondary-color); } -main h6 { - margin: 2em 0 0.5em; - font-size: 1em; +main h5, main h6 { + margin: 1em 0 0.5em; + font-size: 1.1em; + color: var(--secondary-color); } main p { - margin: 0 0 0.5em; - line-height: 1.4em; + line-height: 1.5em; + font-weight: 400; } main pre { @@ -449,6 +503,8 @@ main dl { main dt { margin-bottom: 0.5em; + margin-right: 1em; + float: left; font-weight: bold; } @@ -495,7 +551,7 @@ main header h3 { .constants-list > dl dt { margin-bottom: 0.75em; padding-left: 0; - font-family: "Source Code Pro", Monaco, monospace; + font-family: var(--font-code); font-size: 110%; } @@ -542,7 +598,7 @@ main .method-source-code.active-menu { } main .method-description .method-calls-super { - color: #333; + color: var(--text-color); font-weight: bold; } @@ -558,10 +614,10 @@ main .method-detail:target { main .method-heading { position: relative; - font-family: "Source Code Pro", Monaco, monospace; + font-family: var(--font-code); font-size: 110%; font-weight: bold; - color: #333; + color: var(--text-color); } main .method-heading :link, main .method-heading :visited { @@ -589,7 +645,7 @@ main .method-alias .method-heading { main .method-description, main .aliases { margin-top: 0.75em; - color: #333; + color: var(--text-color); } main .aliases { @@ -613,38 +669,14 @@ main .attribute-access-type { /* @end */ -/* @group Source Code */ - -pre { - margin: 0.5em 0; - border: 1px dashed #999; - padding: 0.5em; - background: #262626; - color: white; - overflow: auto; -} - -.ruby-constant { color: #7fffd4; background: transparent; } -.ruby-keyword { color: #00ffff; background: transparent; } -.ruby-ivar { color: #eedd82; background: transparent; } -.ruby-operator { color: #00ffee; background: transparent; } -.ruby-identifier { color: #ffdead; background: transparent; } -.ruby-node { color: #ffa07a; background: transparent; } -.ruby-comment { color: #dc0000; background: transparent; } -.ruby-regexp { color: #ffa07a; background: transparent; } -.ruby-value { color: #7fffd4; background: transparent; } - -/* @end */ - - /* @group search results */ #search-results { - font-family: Lato, sans-serif; + font-family: var(--font-primary); font-weight: 300; } #search-results .search-match { - font-family: Helvetica, sans-serif; + font-family: var(--font-heading); font-weight: normal; } @@ -680,8 +712,34 @@ pre { #search-results pre { margin: 0.5em; - font-family: "Source Code Pro", Monaco, monospace; + font-family: var(--font-code); } +@media (max-width: 480px) { + nav { + width: 100%; + } + + main { + margin: 1em auto; + padding: 0 1em; + max-width: 100%; + } + + #navigation-toggle { + right: 10px; + left: auto; + } + + #navigation-toggle[aria-expanded="true"] { + left: auto; + } + + table { + display: block; + overflow-x: auto; + white-space: nowrap; + } +} /* @end */ diff --git a/lib/rdoc/generator/template/darkfish/index.rhtml b/lib/rdoc/generator/template/darkfish/index.rhtml index 423e225b68..a5c0dd54da 100644 --- a/lib/rdoc/generator/template/darkfish/index.rhtml +++ b/lib/rdoc/generator/template/darkfish/index.rhtml @@ -1,15 +1,16 @@ <body id="top" role="document" class="file"> -<nav role="navigation"> +<%= render '_sidebar_toggle.rhtml' %> + +<nav id="navigation" role="navigation"> <div id="project-navigation"> <%= render '_sidebar_navigation.rhtml' %> - <%= render '_sidebar_search.rhtml' %> </div> - <div id="project-metadata"> - <%= render '_sidebar_pages.rhtml' %> - <%= render '_sidebar_classes.rhtml' %> - </div> + <%= render '_sidebar_pages.rhtml' %> + <%= render '_sidebar_classes.rhtml' %> + + <%= render '_footer.rhtml' %> </nav> <main role="main"> diff --git a/lib/rdoc/generator/template/darkfish/js/darkfish.js b/lib/rdoc/generator/template/darkfish/js/darkfish.js index 19a85c54e1..bea0a5f1cb 100644 --- a/lib/rdoc/generator/template/darkfish/js/darkfish.js +++ b/lib/rdoc/generator/template/darkfish/js/darkfish.js @@ -90,8 +90,25 @@ function hookFocus() { }); } +function hookSidebar() { + var navigation = document.querySelector('#navigation'); + var navigationToggle = document.querySelector('#navigation-toggle'); + + navigationToggle.addEventListener('click', function() { + navigation.hidden = !navigation.hidden; + navigationToggle.ariaExpanded = navigationToggle.ariaExpanded !== 'true'; + }); + + var isSmallViewport = window.matchMedia("(max-width: 1024px)").matches; + if (isSmallViewport) { + navigation.hidden = true; + navigationToggle.ariaExpanded = false; + } +} + document.addEventListener('DOMContentLoaded', function() { hookSourceViews(); hookSearch(); hookFocus(); + hookSidebar(); }); diff --git a/lib/rdoc/generator/template/darkfish/page.rhtml b/lib/rdoc/generator/template/darkfish/page.rhtml index 4a6b006bb3..fb33eba6fd 100644 --- a/lib/rdoc/generator/template/darkfish/page.rhtml +++ b/lib/rdoc/generator/template/darkfish/page.rhtml @@ -1,18 +1,18 @@ <body id="top" role="document" class="file"> -<nav role="navigation"> +<%= render '_sidebar_toggle.rhtml' %> + +<nav id="navigation" role="navigation"> <div id="project-navigation"> <%= render '_sidebar_navigation.rhtml' %> <%= render '_sidebar_search.rhtml' %> </div> <%= render '_sidebar_table_of_contents.rhtml' %> + <%= render '_sidebar_pages.rhtml' %> - <div id="project-metadata"> - <%= render '_sidebar_pages.rhtml' %> - </div> + <%= render '_footer.rhtml' %> </nav> <main role="main" aria-label="Page <%=h file.full_name%>"> <%= file.description %> </main> - diff --git a/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml b/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml index f0841572c3..098b589add 100644 --- a/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml +++ b/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml @@ -1,13 +1,16 @@ <body role="document"> -<nav role="navigation"> - <%= render '_sidebar_navigation.rhtml' %> +<%= render '_sidebar_toggle.rhtml' %> - <%= render '_sidebar_search.rhtml' %> - - <div id="project-metadata"> - <%= render '_sidebar_pages.rhtml' %> - <%= render '_sidebar_classes.rhtml' %> +<nav id="navigation" role="navigation"> + <div id="project-navigation"> + <%= render '_sidebar_navigation.rhtml' %> + <%= render '_sidebar_search.rhtml' %> </div> + + <%= render '_sidebar_pages.rhtml' %> + <%= render '_sidebar_classes.rhtml' %> + + <%= render '_footer.rhtml' %> </nav> <main role="main"> @@ -15,4 +18,3 @@ <p><%= message %> </main> - diff --git a/lib/rdoc/generator/template/darkfish/servlet_root.rhtml b/lib/rdoc/generator/template/darkfish/servlet_root.rhtml index cab3092b17..373e0006d9 100644 --- a/lib/rdoc/generator/template/darkfish/servlet_root.rhtml +++ b/lib/rdoc/generator/template/darkfish/servlet_root.rhtml @@ -1,5 +1,7 @@ <body role="document"> -<nav role="navigation"> +<%= render '_sidebar_toggle.rhtml' %> + +<nav id="navigation" role="navigation"> <div id="project-navigation"> <div id="home-section" class="nav-section"> <h2> @@ -10,7 +12,8 @@ <%= render '_sidebar_search.rhtml' %> </div> -<%= render '_sidebar_installed.rhtml' %> + <%= render '_sidebar_installed.rhtml' %> + <%= render '_footer.rhtml' %> </nav> <main role="main"> diff --git a/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml b/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml index 54a376c9e5..2cd2207836 100644 --- a/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +++ b/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml @@ -1,4 +1,15 @@ <body id="top" class="table-of-contents"> +<%= render '_sidebar_toggle.rhtml' %> + +<nav id="navigation" role="navigation"> + <div id="project-navigation"> + <%= render '_sidebar_navigation.rhtml' %> + + <%= render '_sidebar_search.rhtml' %> + </div> + + <%= render '_footer.rhtml' %> +</nav> <main role="main"> <h1 class="class"><%= h @title %></h1> diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb index 5c72a5f224..881d19ecb4 100644 --- a/lib/rdoc/markdown.rb +++ b/lib/rdoc/markdown.rb @@ -6,7 +6,7 @@ # RDoc::Markdown as described by the [markdown syntax][syntax]. # # To choose Markdown as your only default format see -# RDoc::Options@Saved+Options for instructions on setting up a `.doc_options` +# RDoc::Options@Saved+Options for instructions on setting up a `.rdoc_options` # file to store your project default. # # ## Usage @@ -1158,7 +1158,7 @@ class RDoc::Markdown return _tmp end - # AtxHeading = AtxStart:s @Sp AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) } + # AtxHeading = AtxStart:s @Spacechar+ AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) } def _AtxHeading _save = self.pos @@ -1169,12 +1169,22 @@ class RDoc::Markdown self.pos = _save break end - _tmp = _Sp() + _save1 = self.pos + _tmp = _Spacechar() + if _tmp + while true + _tmp = _Spacechar() + break unless _tmp + end + _tmp = true + else + self.pos = _save1 + end unless _tmp self.pos = _save break end - _save1 = self.pos + _save2 = self.pos _ary = [] _tmp = apply(:_AtxInline) if _tmp @@ -1187,37 +1197,37 @@ class RDoc::Markdown _tmp = true @result = _ary else - self.pos = _save1 + self.pos = _save2 end a = @result unless _tmp self.pos = _save break end - _save2 = self.pos - _save3 = self.pos + + _save4 = self.pos while true # sequence _tmp = _Sp() unless _tmp - self.pos = _save3 + self.pos = _save4 break end _tmp = scan(/\G(?-mix:#*)/) unless _tmp - self.pos = _save3 + self.pos = _save4 break end _tmp = _Sp() unless _tmp - self.pos = _save3 + self.pos = _save4 end break end # end sequence unless _tmp _tmp = true - self.pos = _save2 + self.pos = _save3 end unless _tmp self.pos = _save @@ -16539,7 +16549,7 @@ class RDoc::Markdown Rules[:_Plain] = rule_info("Plain", "Inlines:a { paragraph a }") Rules[:_AtxInline] = rule_info("AtxInline", "!@Newline !(@Sp /\#*/ @Sp @Newline) Inline") Rules[:_AtxStart] = rule_info("AtxStart", "< /\\\#{1,6}/ > { text.length }") - Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Sp AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }") + Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Spacechar+ AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }") Rules[:_SetextHeading] = rule_info("SetextHeading", "(SetextHeading1 | SetextHeading2)") Rules[:_SetextBottom1] = rule_info("SetextBottom1", "/={1,}/ @Newline") Rules[:_SetextBottom2] = rule_info("SetextBottom2", "/-{1,}/ @Newline") diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb index 6e93030965..3c29870d8a 100644 --- a/lib/rdoc/markup.rb +++ b/lib/rdoc/markup.rb @@ -10,19 +10,24 @@ # RDoc::Markup and other markup formats do no output formatting, this is # handled by the RDoc::Markup::Formatter subclasses. # -# = Supported Formats -# -# Besides the RDoc::Markup format, the following formats are built in to RDoc: -# -# markdown:: -# The markdown format as described by -# http://daringfireball.net/projects/markdown/. See RDoc::Markdown for -# details on the parser and supported extensions. -# rd:: -# The rdtool format. See RDoc::RD for details on the parser and format. -# tomdoc:: -# The TomDoc format as described by http://tomdoc.org/. See RDoc::TomDoc -# for details on the parser and supported extensions. +# = Markup Formats +# +# +RDoc+ supports these markup formats: +# +# - +rdoc+: +# the +RDoc+ markup format; +# see RDoc::MarkupReference. +# - +markdown+: +# The +markdown+ markup format as described in +# the {Markdown Guide}[https://www.markdownguide.org]; +# see RDoc::Markdown. +# - +rd+: +# the +rd+ markup format format; +# see RDoc::RD. +# - +tomdoc+: +# the TomDoc format as described in +# {TomDoc for Ruby}[http://tomdoc.org]; +# see RDoc::TomDoc. # # You can choose a markup format using the following methods: # diff --git a/lib/rdoc/markup/attribute_manager.rb b/lib/rdoc/markup/attribute_manager.rb index f6eb06da95..ed014f255b 100644 --- a/lib/rdoc/markup/attribute_manager.rb +++ b/lib/rdoc/markup/attribute_manager.rb @@ -260,7 +260,7 @@ class RDoc::Markup::AttributeManager def add_word_pair(start, stop, name, exclusive = false) raise ArgumentError, "Word flags may not start with '<'" if - start[0,1] == '<' + start[0, 1] == '<' bitmap = @attributes.bitmap_for name @@ -271,7 +271,7 @@ class RDoc::Markup::AttributeManager @word_pair_map[pattern] = bitmap end - @protectable << start[0,1] + @protectable << start[0, 1] @protectable.uniq! @exclusive_bitmap |= bitmap if exclusive diff --git a/lib/rdoc/markup/pre_process.rb b/lib/rdoc/markup/pre_process.rb index 88078c9cef..979f2eadae 100644 --- a/lib/rdoc/markup/pre_process.rb +++ b/lib/rdoc/markup/pre_process.rb @@ -97,15 +97,18 @@ class RDoc::Markup::PreProcess # RDoc::CodeObject#metadata for details. def handle text, code_object = nil, &block + first_line = 1 if RDoc::Comment === text then comment = text text = text.text + first_line = comment.line || 1 end # regexp helper (square brackets for optional) # $1 $2 $3 $4 $5 # [prefix][\]:directive:[spaces][param]newline - text = text.gsub(/^([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):(\w+):([ \t]*)(.+)?(\r?\n|$)/) do + text = text.lines.map.with_index(first_line) do |line, num| + next line unless line =~ /\A([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/ # skip something like ':toto::' next $& if $4.empty? and $5 and $5[0, 1] == ':' @@ -120,8 +123,8 @@ class RDoc::Markup::PreProcess next "#{$1.strip}\n" end - handle_directive $1, $3, $5, code_object, text.encoding, &block - end + handle_directive $1, $3, $5, code_object, text.encoding, num, &block + end.join if comment then comment.text = text @@ -148,7 +151,7 @@ class RDoc::Markup::PreProcess # When 1.8.7 support is ditched prefix can be defaulted to '' def handle_directive prefix, directive, param, code_object = nil, - encoding = nil + encoding = nil, line = nil blankline = "#{prefix.strip}\n" directive = directive.downcase @@ -220,11 +223,11 @@ class RDoc::Markup::PreProcess # remove parameter &block code_object.params = code_object.params.sub(/,?\s*&\w+/, '') if code_object.params - code_object.block_params = param + code_object.block_params = param || '' blankline else - result = yield directive, param if block_given? + result = yield directive, param, line if block_given? case result when nil then diff --git a/lib/rdoc/markup/to_bs.rb b/lib/rdoc/markup/to_bs.rb index afd9d6e981..b7b73e73f7 100644 --- a/lib/rdoc/markup/to_bs.rb +++ b/lib/rdoc/markup/to_bs.rb @@ -24,7 +24,7 @@ class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc def init_tags add_tag :BOLD, '+b', '-b' add_tag :EM, '+_', '-_' - add_tag :TT, '' , '' # we need in_tt information maintained + add_tag :TT, '', '' # we need in_tt information maintained end ## diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index 7518e6cc54..2631d57364 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -233,9 +233,9 @@ class RDoc::Options attr_accessor :main_page ## - # The default markup format. The default is 'rdoc'. 'markdown', 'tomdoc' - # and 'rd' are also built-in. - + # The markup format. + # One of: +rdoc+ (the default), +markdown+, +rd+, +tomdoc+. + # See {Markup Formats}[rdoc-ref:RDoc::Markup@Markup+Formats]. attr_accessor :markup ## @@ -683,7 +683,7 @@ Usage: #{opt.program_name} [options] [names...] EOF - parsers = Hash.new { |h,parser| h[parser] = [] } + parsers = Hash.new { |h, parser| h[parser] = [] } RDoc::Parser.parsers.each do |regexp, parser| parsers[parser.name.sub('RDoc::Parser::', '')] << regexp.source diff --git a/lib/rdoc/parser.rb b/lib/rdoc/parser.rb index 425105effa..76801ba377 100644 --- a/lib/rdoc/parser.rb +++ b/lib/rdoc/parser.rb @@ -166,7 +166,8 @@ class RDoc::Parser # Finds and instantiates the correct parser for the given +file_name+ and # +content+. - def self.for top_level, file_name, content, options, stats + def self.for top_level, content, options, stats + file_name = top_level.absolute_name return if binary? file_name parser = use_markup content diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb index f8f238fd74..4050d7aa49 100644 --- a/lib/rdoc/parser/c.rb +++ b/lib/rdoc/parser/c.rb @@ -440,7 +440,7 @@ class RDoc::Parser::C < RDoc::Parser # Scans #content for rb_include_module def do_includes - @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c,m| + @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c, m| next unless cls = @classes[c] m = @known_classes[m] || m diff --git a/lib/rdoc/parser/changelog.rb b/lib/rdoc/parser/changelog.rb index a046241870..12a50f8d0e 100644 --- a/lib/rdoc/parser/changelog.rb +++ b/lib/rdoc/parser/changelog.rb @@ -193,7 +193,7 @@ class RDoc::Parser::ChangeLog < RDoc::Parser entries << [entry_name, entry_body] if entry_name - entries.reject! do |(entry,_)| + entries.reject! do |(entry, _)| entry == nil end @@ -221,7 +221,7 @@ class RDoc::Parser::ChangeLog < RDoc::Parser module Git ## - # Parses auxiliary info. Currentry `base-url` to expand + # Parses auxiliary info. Currently `base-url` to expand # references is effective. def parse_info(info) diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb new file mode 100644 index 0000000000..05e98ad6c4 --- /dev/null +++ b/lib/rdoc/parser/prism_ruby.rb @@ -0,0 +1,1026 @@ +# frozen_string_literal: true + +require 'prism' +require_relative 'ripper_state_lex' + +# Unlike lib/rdoc/parser/ruby.rb, this file is not based on rtags and does not contain code from +# rtags.rb - +# ruby-lex.rb - ruby lexcal analyzer +# ruby-token.rb - ruby tokens + +# Parse and collect document from Ruby source code. +# RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it. + +class RDoc::Parser::PrismRuby < RDoc::Parser + + parse_files_matching(/\.rbw?$/) if ENV['RDOC_USE_PRISM_PARSER'] + + attr_accessor :visibility + attr_reader :container, :singleton + + def initialize(top_level, file_name, content, options, stats) + super + + content = handle_tab_width(content) + + @size = 0 + @token_listeners = nil + content = RDoc::Encoding.remove_magic_comment content + @content = content + @markup = @options.markup + @track_visibility = :nodoc != @options.visibility + @encoding = @options.encoding + + @module_nesting = [top_level] + @container = top_level + @visibility = :public + @singleton = false + end + + # Dive into another container + + def with_container(container, singleton: false) + old_container = @container + old_visibility = @visibility + old_singleton = @singleton + @visibility = :public + @container = container + @singleton = singleton + unless singleton + @module_nesting.push container + + # Need to update module parent chain to emulate Module.nesting. + # This mechanism is inaccurate and needs to be fixed. + container.parent = old_container + end + yield container + ensure + @container = old_container + @visibility = old_visibility + @singleton = old_singleton + @module_nesting.pop unless singleton + end + + # Records the location of this +container+ in the file for this parser and + # adds it to the list of classes and modules in the file. + + def record_location container # :nodoc: + case container + when RDoc::ClassModule then + @top_level.add_to_classes_or_modules container + end + + container.record_location @top_level + end + + # Scans this Ruby file for Ruby constructs + + def scan + @tokens = RDoc::Parser::RipperStateLex.parse(@content) + @lines = @content.lines + result = Prism.parse(@content) + @program_node = result.value + @line_nodes = {} + prepare_line_nodes(@program_node) + prepare_comments(result.comments) + return if @top_level.done_documenting + + @first_non_meta_comment = nil + if (_line_no, start_line, rdoc_comment = @unprocessed_comments.first) + @first_non_meta_comment = rdoc_comment if start_line < @program_node.location.start_line + end + + @program_node.accept(RDocVisitor.new(self, @top_level, @store)) + process_comments_until(@lines.size + 1) + end + + def should_document?(code_object) # :nodoc: + return true unless @track_visibility + return false if code_object.parent&.document_children == false + code_object.document_self + end + + # Assign AST node to a line. + # This is used to show meta-method source code in the documentation. + + def prepare_line_nodes(node) # :nodoc: + case node + when Prism::CallNode, Prism::DefNode + @line_nodes[node.location.start_line] ||= node + end + node.compact_child_nodes.each do |child| + prepare_line_nodes(child) + end + end + + # Prepares comments for processing. Comments are grouped into consecutive. + # Consecutive comment is linked to the next non-blank line. + # + # Example: + # 01| class A # modifier comment 1 + # 02| def foo; end # modifier comment 2 + # 03| + # 04| # consecutive comment 1 start_line: 4 + # 05| # consecutive comment 1 linked to line: 7 + # 06| + # 07| # consecutive comment 2 start_line: 7 + # 08| # consecutive comment 2 linked to line: 10 + # 09| + # 10| def bar; end # consecutive comment 2 linked to this line + # 11| end + + def prepare_comments(comments) + current = [] + consecutive_comments = [current] + @modifier_comments = {} + comments.each do |comment| + if comment.is_a? Prism::EmbDocComment + consecutive_comments << [comment] << (current = []) + elsif comment.location.start_line_slice.match?(/\S/) + @modifier_comments[comment.location.start_line] = RDoc::Comment.new(comment.slice, @top_level, :ruby) + elsif current.empty? || current.last.location.end_line + 1 == comment.location.start_line + current << comment + else + consecutive_comments << (current = [comment]) + end + end + consecutive_comments.reject!(&:empty?) + + # Example: line_no = 5, start_line = 2, comment_text = "# comment_start_line\n# comment\n" + # 1| class A + # 2| # comment_start_line + # 3| # comment + # 4| + # 5| def f; end # comment linked to this line + # 6| end + @unprocessed_comments = consecutive_comments.map! do |comments| + start_line = comments.first.location.start_line + line_no = comments.last.location.end_line + (comments.last.location.end_column == 0 ? 0 : 1) + texts = comments.map do |c| + c.is_a?(Prism::EmbDocComment) ? c.slice.lines[1...-1].join : c.slice + end + text = RDoc::Encoding.change_encoding(texts.join("\n"), @encoding) if @encoding + line_no += 1 while @lines[line_no - 1]&.match?(/\A\s*$/) + comment = RDoc::Comment.new(text, @top_level, :ruby) + comment.line = start_line + [line_no, start_line, comment] + end + + # The first comment is special. It defines markup for the rest of the comments. + _, first_comment_start_line, first_comment_text = @unprocessed_comments.first + if first_comment_text && @lines[0...first_comment_start_line - 1].all? { |l| l.match?(/\A\s*$/) } + comment = RDoc::Comment.new(first_comment_text.text, @top_level, :ruby) + handle_consecutive_comment_directive(@container, comment) + @markup = comment.format + end + @unprocessed_comments.each do |_, _, comment| + comment.format = @markup + end + end + + # Creates an RDoc::Method on +container+ from +comment+ if there is a + # Signature section in the comment + + def parse_comment_tomdoc(container, comment, line_no, start_line) + return unless signature = RDoc::TomDoc.signature(comment) + + name, = signature.split %r%[ \(]%, 2 + + meth = RDoc::GhostMethod.new comment.text, name + record_location(meth) + meth.line = start_line + meth.call_seq = signature + return unless meth.name + + meth.start_collecting_tokens + node = @line_nodes[line_no] + tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)] + tokens.each { |token| meth.token_stream << token } + + container.add_method meth + comment.remove_private + comment.normalize + meth.comment = comment + @stats.add_method meth + end + + def handle_modifier_directive(code_object, line_no) # :nodoc: + comment = @modifier_comments[line_no] + @preprocess.handle(comment.text, code_object) if comment + end + + def handle_consecutive_comment_directive(code_object, comment) # :nodoc: + return unless comment + @preprocess.handle(comment, code_object) do |directive, param| + case directive + when 'method', 'singleton-method', + 'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then + # handled elsewhere + '' + when 'section' then + @container.set_current_section(param, comment.dup) + comment.text = '' + break + end + end + comment.remove_private + end + + def call_node_name_arguments(call_node) # :nodoc: + return [] unless call_node.arguments + call_node.arguments.arguments.map do |arg| + case arg + when Prism::SymbolNode + arg.value + when Prism::StringNode + arg.unescaped + end + end || [] + end + + # Handles meta method comments + + def handle_meta_method_comment(comment, node) + is_call_node = node.is_a?(Prism::CallNode) + singleton_method = false + visibility = @visibility + attributes = rw = line_no = method_name = nil + + processed_comment = comment.dup + @preprocess.handle(processed_comment, @container) do |directive, param, line| + case directive + when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor' + attributes = [param] if param + attributes ||= call_node_name_arguments(node) if is_call_node + rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R' + '' + when 'method' + method_name = param + line_no = line + '' + when 'singleton-method' + method_name = param + line_no = line + singleton_method = true + visibility = :public + '' + when 'section' then + @container.set_current_section(param, comment.dup) + return # If the comment contains :section:, it is not a meta method comment + end + end + + if attributes + attributes.each do |attr| + a = RDoc::Attr.new(@container, attr, rw, processed_comment) + a.store = @store + a.line = line_no + a.singleton = @singleton + record_location(a) + @container.add_attribute(a) + a.visibility = visibility + end + elsif line_no || node + method_name ||= call_node_name_arguments(node).first if is_call_node + meth = RDoc::AnyMethod.new(@container, method_name) + meth.singleton = @singleton || singleton_method + handle_consecutive_comment_directive(meth, comment) + comment.normalize + comment.extract_call_seq(meth) + meth.comment = comment + if node + tokens = visible_tokens_from_location(node.location) + line_no = node.location.start_line + else + tokens = [file_line_comment_token(line_no)] + end + internal_add_method( + @container, + meth, + line_no: line_no, + visibility: visibility, + singleton: @singleton || singleton_method, + params: '()', + calls_super: false, + block_params: nil, + tokens: tokens + ) + end + end + + def normal_comment_treat_as_ghost_method_for_now?(comment_text, line_no) # :nodoc: + # Meta method comment should start with `##` but some comments does not follow this rule. + # For now, RDoc accepts them as a meta method comment if there is no node linked to it. + !@line_nodes[line_no] && comment_text.match?(/^#\s+:(method|singleton-method|attr|attr_reader|attr_writer|attr_accessor):/) + end + + def handle_standalone_consecutive_comment_directive(comment, line_no, start_line) # :nodoc: + if @markup == 'tomdoc' + parse_comment_tomdoc(@container, comment, line_no, start_line) + return + end + + if comment.text =~ /\A#\#$/ && comment != @first_non_meta_comment + node = @line_nodes[line_no] + handle_meta_method_comment(comment, node) + elsif normal_comment_treat_as_ghost_method_for_now?(comment.text, line_no) && comment != @first_non_meta_comment + handle_meta_method_comment(comment, nil) + else + handle_consecutive_comment_directive(@container, comment) + end + end + + # Processes consecutive comments that were not linked to any documentable code until the given line number + + def process_comments_until(line_no_until) + while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until + line_no, start_line, rdoc_comment = @unprocessed_comments.shift + handle_standalone_consecutive_comment_directive(rdoc_comment, line_no, start_line) + end + end + + # Skips all undocumentable consecutive comments until the given line number. + # Undocumentable comments are comments written inside `def` or inside undocumentable class/module + + def skip_comments_until(line_no_until) + while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until + @unprocessed_comments.shift + end + end + + # Returns consecutive comment linked to the given line number + + def consecutive_comment(line_no) + if @unprocessed_comments.first&.first == line_no + @unprocessed_comments.shift.last + end + end + + def slice_tokens(start_pos, end_pos) # :nodoc: + start_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> start_pos) >= 0 } + end_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> end_pos) >= 0 } + tokens = @tokens[start_index...end_index] + tokens.pop if tokens.last&.kind == :on_nl + tokens + end + + def file_line_comment_token(line_no) # :nodoc: + position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no - 1, 0, :on_comment) + position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}" + position_comment + end + + # Returns tokens from the given location + + def visible_tokens_from_location(location) + position_comment = file_line_comment_token(location.start_line) + newline_token = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n") + indent_token = RDoc::Parser::RipperStateLex::Token.new(location.start_line, 0, :on_sp, ' ' * location.start_character_column) + tokens = slice_tokens( + [location.start_line, location.start_character_column], + [location.end_line, location.end_character_column] + ) + [position_comment, newline_token, indent_token, *tokens] + end + + # Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar` + + def change_method_visibility(names, visibility, singleton: @singleton) + new_methods = [] + @container.methods_matching(names, singleton) do |m| + if m.parent != @container + m = m.dup + record_location(m) + new_methods << m + else + m.visibility = visibility + end + end + new_methods.each do |method| + case method + when RDoc::AnyMethod then + @container.add_method(method) + when RDoc::Attr then + @container.add_attribute(method) + end + method.visibility = visibility + end + end + + # Handles `module_function :foo, :bar` + + def change_method_to_module_function(names) + @container.set_visibility_for(names, :private, false) + new_methods = [] + @container.methods_matching(names) do |m| + s_m = m.dup + record_location(s_m) + s_m.singleton = true + new_methods << s_m + end + new_methods.each do |method| + case method + when RDoc::AnyMethod then + @container.add_method(method) + when RDoc::Attr then + @container.add_attribute(method) + end + method.visibility = :public + end + end + + # Handles `alias foo bar` and `alias_method :foo, :bar` + + def add_alias_method(old_name, new_name, line_no) + comment = consecutive_comment(line_no) + handle_consecutive_comment_directive(@container, comment) + visibility = @container.find_method(old_name, @singleton)&.visibility || :public + a = RDoc::Alias.new(nil, old_name, new_name, comment, @singleton) + a.comment = comment + handle_modifier_directive(a, line_no) + a.store = @store + a.line = line_no + record_location(a) + if should_document?(a) + @container.add_alias(a) + @container.find_method(new_name, @singleton)&.visibility = visibility + end + end + + # Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b` + + def add_attributes(names, rw, line_no) + comment = consecutive_comment(line_no) + handle_consecutive_comment_directive(@container, comment) + return unless @container.document_children + + names.each do |symbol| + a = RDoc::Attr.new(nil, symbol.to_s, rw, comment) + a.store = @store + a.line = line_no + a.singleton = @singleton + record_location(a) + handle_modifier_directive(a, line_no) + @container.add_attribute(a) if should_document?(a) + a.visibility = visibility # should set after adding to container + end + end + + def add_includes_extends(names, rdoc_class, line_no) # :nodoc: + comment = consecutive_comment(line_no) + handle_consecutive_comment_directive(@container, comment) + names.each do |name| + ie = @container.add(rdoc_class, name, '') + ie.store = @store + ie.line = line_no + ie.comment = comment + record_location(ie) + end + end + + # Handle `include Foo, Bar` + + def add_includes(names, line_no) # :nodoc: + add_includes_extends(names, RDoc::Include, line_no) + end + + # Handle `extend Foo, Bar` + + def add_extends(names, line_no) # :nodoc: + add_includes_extends(names, RDoc::Extend, line_no) + end + + # Adds a method defined by `def` syntax + + def add_method(name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, end_line:) + receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container + meth = RDoc::AnyMethod.new(nil, name) + if (comment = consecutive_comment(start_line)) + handle_consecutive_comment_directive(@container, comment) + handle_consecutive_comment_directive(meth, comment) + + comment.normalize + comment.extract_call_seq(meth) + meth.comment = comment + end + handle_modifier_directive(meth, start_line) + handle_modifier_directive(meth, end_line) + return unless should_document?(meth) + + + if meth.name == 'initialize' && !singleton + if meth.dont_rename_initialize + visibility = :protected + else + meth.name = 'new' + singleton = true + visibility = :public + end + end + + internal_add_method( + receiver, + meth, + line_no: start_line, + visibility: visibility, + singleton: singleton, + params: params, + calls_super: calls_super, + block_params: block_params, + tokens: tokens + ) + end + + private def internal_add_method(container, meth, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc: + meth.name ||= meth.call_seq[/\A[^()\s]+/] if meth.call_seq + meth.name ||= 'unknown' + meth.store = @store + meth.line = line_no + meth.singleton = singleton + container.add_method(meth) # should add after setting singleton and before setting visibility + meth.visibility = visibility + meth.params ||= params + meth.calls_super = calls_super + meth.block_params ||= block_params if block_params + record_location(meth) + meth.start_collecting_tokens + tokens.each do |token| + meth.token_stream << token + end + end + + # Find or create module or class from a given module name. + # If module or class does not exist, creates a module or a class according to `create_mode` argument. + + def find_or_create_module_path(module_name, create_mode) + root_name, *path, name = module_name.split('::') + add_module = ->(mod, name, mode) { + case mode + when :class + mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store } + when :module + mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store } + end + } + if root_name.empty? + mod = @top_level + else + @module_nesting.reverse_each do |nesting| + mod = nesting.find_module_named(root_name) + break if mod + end + return mod || add_module.call(@top_level, root_name, create_mode) unless name + mod ||= add_module.call(@top_level, root_name, :module) + end + path.each do |name| + mod = mod.find_module_named(name) || add_module.call(mod, name, :module) + end + mod.find_module_named(name) || add_module.call(mod, name, create_mode) + end + + # Resolves constant path to a full path by searching module nesting + + def resolve_constant_path(constant_path) + owner_name, path = constant_path.split('::', 2) + return constant_path if owner_name.empty? # ::Foo, ::Foo::Bar + mod = nil + @module_nesting.reverse_each do |nesting| + mod = nesting.find_module_named(owner_name) + break if mod + end + mod ||= @top_level.find_module_named(owner_name) + [mod.full_name, path].compact.join('::') if mod + end + + # Returns a pair of owner module and constant name from a given constant path. + # Creates owner module if it does not exist. + + def find_or_create_constant_owner_name(constant_path) + const_path, colon, name = constant_path.rpartition('::') + if colon.empty? # class Foo + [@container, name] + elsif const_path.empty? # class ::Foo + [@top_level, name] + else # `class Foo::Bar` or `class ::Foo::Bar` + [find_or_create_module_path(const_path, :module), name] + end + end + + # Adds a constant + + def add_constant(constant_name, rhs_name, start_line, end_line) + comment = consecutive_comment(start_line) + handle_consecutive_comment_directive(@container, comment) + owner, name = find_or_create_constant_owner_name(constant_name) + constant = RDoc::Constant.new(name, rhs_name, comment) + constant.store = @store + constant.line = start_line + record_location(constant) + handle_modifier_directive(constant, start_line) + handle_modifier_directive(constant, end_line) + owner.add_constant(constant) + mod = + if rhs_name =~ /^::/ + @store.find_class_or_module(rhs_name) + else + @container.find_module_named(rhs_name) + end + if mod && constant.document_self + a = @container.add_module_alias(mod, rhs_name, constant, @top_level) + a.store = @store + a.line = start_line + record_location(a) + end + end + + # Adds module or class + + def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil) + comment = consecutive_comment(start_line) + handle_consecutive_comment_directive(@container, comment) + return unless @container.document_children + + owner, name = find_or_create_constant_owner_name(module_name) + if is_class + mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object') + + # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information. + # We need to fix it when RDoc::NormalClass resolved to a wrong constant name + if superclass_name + superclass_full_path = resolve_constant_path(superclass_name) + superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path + superclass_full_path ||= superclass_name + if superclass + mod.superclass = superclass + elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path + mod.superclass = superclass_full_path + end + end + else + mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name) + end + + mod.store = @store + mod.line = start_line + record_location(mod) + handle_modifier_directive(mod, start_line) + handle_modifier_directive(mod, end_line) + mod.add_comment(comment, @top_level) if comment + mod + end + + class RDocVisitor < Prism::Visitor # :nodoc: + def initialize(scanner, top_level, store) + @scanner = scanner + @top_level = top_level + @store = store + end + + def visit_call_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + if node.receiver.nil? + case node.name + when :attr + _visit_call_attr_reader_writer_accessor(node, 'R') + when :attr_reader + _visit_call_attr_reader_writer_accessor(node, 'R') + when :attr_writer + _visit_call_attr_reader_writer_accessor(node, 'W') + when :attr_accessor + _visit_call_attr_reader_writer_accessor(node, 'RW') + when :include + _visit_call_include(node) + when :extend + _visit_call_extend(node) + when :public + _visit_call_public_private_protected(node, :public) { super } + when :private + _visit_call_public_private_protected(node, :private) { super } + when :protected + _visit_call_public_private_protected(node, :protected) { super } + when :private_constant + _visit_call_private_constant(node) + when :public_constant + _visit_call_public_constant(node) + when :require + _visit_call_require(node) + when :alias_method + _visit_call_alias_method(node) + when :module_function + _visit_call_module_function(node) { super } + when :public_class_method + _visit_call_public_private_class_method(node, :public) { super } + when :private_class_method + _visit_call_public_private_class_method(node, :private) { super } + else + super + end + else + super + end + end + + def visit_alias_method_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + return unless node.old_name.is_a?(Prism::SymbolNode) && node.new_name.is_a?(Prism::SymbolNode) + @scanner.add_alias_method(node.old_name.value.to_s, node.new_name.value.to_s, node.location.start_line) + end + + def visit_module_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + module_name = constant_path_string(node.constant_path) + mod = @scanner.add_module_or_class(module_name, node.location.start_line, node.location.end_line) if module_name + if mod + @scanner.with_container(mod) do + super + @scanner.process_comments_until(node.location.end_line) + end + else + @scanner.skip_comments_until(node.location.end_line) + end + end + + def visit_class_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + superclass_name = constant_path_string(node.superclass) if node.superclass + class_name = constant_path_string(node.constant_path) + klass = @scanner.add_module_or_class(class_name, node.location.start_line, node.location.end_line, is_class: true, superclass_name: superclass_name) if class_name + if klass + @scanner.with_container(klass) do + super + @scanner.process_comments_until(node.location.end_line) + end + else + @scanner.skip_comments_until(node.location.end_line) + end + end + + def visit_singleton_class_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + + expression = node.expression + expression = expression.body.body.first if expression.is_a?(Prism::ParenthesesNode) && expression.body&.body&.size == 1 + + case expression + when Prism::ConstantWriteNode + # Accept `class << (NameErrorCheckers = Object.new)` as a module which is not actually a module + mod = @scanner.container.add_module(RDoc::NormalModule, expression.name.to_s) + when Prism::ConstantPathNode, Prism::ConstantReadNode + expression_name = constant_path_string(expression) + # If a constant_path does not exist, RDoc creates a module + mod = @scanner.find_or_create_module_path(expression_name, :module) if expression_name + when Prism::SelfNode + mod = @scanner.container if @scanner.container != @top_level + end + if mod + @scanner.with_container(mod, singleton: true) do + super + @scanner.process_comments_until(node.location.end_line) + end + else + @scanner.skip_comments_until(node.location.end_line) + end + end + + def visit_def_node(node) + start_line = node.location.start_line + end_line = node.location.end_line + @scanner.process_comments_until(start_line - 1) + + case node.receiver + when Prism::NilNode, Prism::TrueNode, Prism::FalseNode + visibility = :public + singleton = false + receiver_name = + case node.receiver + when Prism::NilNode + 'NilClass' + when Prism::TrueNode + 'TrueClass' + when Prism::FalseNode + 'FalseClass' + end + receiver_fallback_type = :class + when Prism::SelfNode + # singleton method of a singleton class is not documentable + return if @scanner.singleton + visibility = :public + singleton = true + when Prism::ConstantReadNode, Prism::ConstantPathNode + visibility = :public + singleton = true + receiver_name = constant_path_string(node.receiver) + receiver_fallback_type = :module + return unless receiver_name + when nil + visibility = @scanner.visibility + singleton = @scanner.singleton + else + # `def (unknown expression).method_name` is not documentable + return + end + name = node.name.to_s + params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node) + tokens = @scanner.visible_tokens_from_location(node.location) + + @scanner.add_method( + name, + receiver_name: receiver_name, + receiver_fallback_type: receiver_fallback_type, + visibility: visibility, + singleton: singleton, + params: params, + block_params: block_params, + calls_super: calls_super, + tokens: tokens, + start_line: start_line, + end_line: end_line + ) + ensure + @scanner.skip_comments_until(end_line) + end + + def visit_constant_path_write_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + path = constant_path_string(node.target) + return unless path + + @scanner.add_constant( + path, + constant_path_string(node.value) || node.value.slice, + node.location.start_line, + node.location.end_line + ) + @scanner.skip_comments_until(node.location.end_line) + # Do not traverse rhs not to document `A::B = Struct.new{def undocumentable_method; end}` + end + + def visit_constant_write_node(node) + @scanner.process_comments_until(node.location.start_line - 1) + @scanner.add_constant( + node.name.to_s, + constant_path_string(node.value) || node.value.slice, + node.location.start_line, + node.location.end_line + ) + @scanner.skip_comments_until(node.location.end_line) + # Do not traverse rhs not to document `A = Struct.new{def undocumentable_method; end}` + end + + private + + def constant_arguments_names(call_node) + return unless call_node.arguments + names = call_node.arguments.arguments.map { |arg| constant_path_string(arg) } + names.all? ? names : nil + end + + def symbol_arguments(call_node) + arguments_node = call_node.arguments + return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)} + arguments_node.arguments.map { |arg| arg.value.to_sym } + end + + def visibility_method_arguments(call_node, singleton:) + arguments_node = call_node.arguments + return unless arguments_node + symbols = symbol_arguments(call_node) + if symbols + # module_function :foo, :bar + return symbols.map(&:to_s) + else + return unless arguments_node.arguments.size == 1 + arg = arguments_node.arguments.first + return unless arg.is_a?(Prism::DefNode) + + if singleton + # `private_class_method def foo; end` `private_class_method def not_self.foo; end` should be ignored + return unless arg.receiver.is_a?(Prism::SelfNode) + else + # `module_function def something.foo` should be ignored + return if arg.receiver + end + # `module_function def foo; end` or `private_class_method def self.foo; end` + [arg.name.to_s] + end + end + + def constant_path_string(node) + case node + when Prism::ConstantReadNode + node.name.to_s + when Prism::ConstantPathNode + parent_name = node.parent ? constant_path_string(node.parent) : '' + "#{parent_name}::#{node.name}" if parent_name + end + end + + def _visit_call_require(call_node) + return unless call_node.arguments&.arguments&.size == 1 + arg = call_node.arguments.arguments.first + return unless arg.is_a?(Prism::StringNode) + @scanner.container.add_require(RDoc::Require.new(arg.unescaped, nil)) + end + + def _visit_call_module_function(call_node) + yield + return if @scanner.singleton + names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s) + @scanner.change_method_to_module_function(names) if names + end + + def _visit_call_public_private_class_method(call_node, visibility) + yield + return if @scanner.singleton + names = visibility_method_arguments(call_node, singleton: true) + @scanner.change_method_visibility(names, visibility, singleton: true) if names + end + + def _visit_call_public_private_protected(call_node, visibility) + arguments_node = call_node.arguments + if arguments_node.nil? # `public` `private` + @scanner.visibility = visibility + else # `public :foo, :bar`, `private def foo; end` + yield + names = visibility_method_arguments(call_node, singleton: @scanner.singleton) + @scanner.change_method_visibility(names, visibility) if names + end + end + + def _visit_call_alias_method(call_node) + new_name, old_name, *rest = symbol_arguments(call_node) + return unless old_name && new_name && rest.empty? + @scanner.add_alias_method(old_name.to_s, new_name.to_s, call_node.location.start_line) + end + + def _visit_call_include(call_node) + names = constant_arguments_names(call_node) + line_no = call_node.location.start_line + return unless names + + if @scanner.singleton + @scanner.add_extends(names, line_no) + else + @scanner.add_includes(names, line_no) + end + end + + def _visit_call_extend(call_node) + names = constant_arguments_names(call_node) + @scanner.add_extends(names, call_node.location.start_line) if names && !@scanner.singleton + end + + def _visit_call_public_constant(call_node) + return if @scanner.singleton + names = symbol_arguments(call_node) + @scanner.container.set_constant_visibility_for(names.map(&:to_s), :public) if names + end + + def _visit_call_private_constant(call_node) + return if @scanner.singleton + names = symbol_arguments(call_node) + @scanner.container.set_constant_visibility_for(names.map(&:to_s), :private) if names + end + + def _visit_call_attr_reader_writer_accessor(call_node, rw) + names = symbol_arguments(call_node) + @scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names + end + class MethodSignatureVisitor < Prism::Visitor # :nodoc: + class << self + def scan_signature(def_node) + visitor = new + def_node.body&.accept(visitor) + params = "(#{def_node.parameters&.slice})" + block_params = visitor.yields.first + [params, block_params, visitor.calls_super] + end + end + + attr_reader :params, :yields, :calls_super + + def initialize + @params = nil + @calls_super = false + @yields = [] + end + + def visit_def_node(node) + # stop traverse inside nested def + end + + def visit_yield_node(node) + @yields << (node.arguments&.slice || '') + end + + def visit_super_node(node) + @calls_super = true + super + end + + def visit_forwarding_super_node(node) + @calls_super = true + end + end + end +end diff --git a/lib/rdoc/parser/ripper_state_lex.rb b/lib/rdoc/parser/ripper_state_lex.rb index f6cefd0305..2212906bbd 100644 --- a/lib/rdoc/parser/ripper_state_lex.rb +++ b/lib/rdoc/parser/ripper_state_lex.rb @@ -7,307 +7,12 @@ require 'ripper' class RDoc::Parser::RipperStateLex # :stopdoc: - # TODO: Remove this constants after Ruby 2.4 EOL - RIPPER_HAS_LEX_STATE = Ripper::Filter.method_defined?(:state) - Token = Struct.new(:line_no, :char_no, :kind, :text, :state) - EXPR_NONE = 0 - EXPR_BEG = 1 - EXPR_END = 2 - EXPR_ENDARG = 4 - EXPR_ENDFN = 8 - EXPR_ARG = 16 - EXPR_CMDARG = 32 - EXPR_MID = 64 - EXPR_FNAME = 128 - EXPR_DOT = 256 - EXPR_CLASS = 512 - EXPR_LABEL = 1024 - EXPR_LABELED = 2048 - EXPR_FITEM = 4096 - EXPR_VALUE = EXPR_BEG - EXPR_BEG_ANY = (EXPR_BEG | EXPR_MID | EXPR_CLASS) - EXPR_ARG_ANY = (EXPR_ARG | EXPR_CMDARG) - EXPR_END_ANY = (EXPR_END | EXPR_ENDARG | EXPR_ENDFN) - - class InnerStateLex < Ripper::Filter - attr_accessor :lex_state - - def initialize(code) - @lex_state = EXPR_BEG - @in_fname = false - @continue = false - reset - super(code) - end - - def reset - @command_start = false - @cmd_state = @command_start - end - - def on_nl(tok, data) - case @lex_state - when EXPR_FNAME, EXPR_DOT - @continue = true - else - @continue = false - @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0 - end - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_ignored_nl(tok, data) - case @lex_state - when EXPR_FNAME, EXPR_DOT - @continue = true - else - @continue = false - @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0 - end - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_op(tok, data) - case tok - when '&', '|', '!', '!=', '!~' - case @lex_state - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - when '<<' - # TODO next token? - case @lex_state - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - when '?' - @lex_state = EXPR_BEG - when '&&', '||', '+=', '-=', '*=', '**=', - '&=', '|=', '^=', '<<=', '>>=', '||=', '&&=' - @lex_state = EXPR_BEG - when '::' - case @lex_state - when EXPR_ARG, EXPR_CMDARG - @lex_state = EXPR_DOT - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - else - case @lex_state - when EXPR_FNAME, EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_BEG - end - end - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_kw(tok, data) - case tok - when 'class' - @lex_state = EXPR_CLASS - @in_fname = true - when 'def' - @lex_state = EXPR_FNAME - @continue = true - @in_fname = true - when 'if', 'unless', 'while', 'until' - if ((EXPR_MID | EXPR_END | EXPR_ENDARG | EXPR_ENDFN | EXPR_ARG | EXPR_CMDARG) & @lex_state) != 0 # postfix if - @lex_state = EXPR_BEG | EXPR_LABEL - else - @lex_state = EXPR_BEG - end - when 'begin', 'case', 'when' - @lex_state = EXPR_BEG - when 'return', 'break' - @lex_state = EXPR_MID - else - if @lex_state == EXPR_FNAME - @lex_state = EXPR_END - else - @lex_state = EXPR_END - end - end - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_tstring_beg(tok, data) - @lex_state = EXPR_BEG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_tstring_end(tok, data) - @lex_state = EXPR_END | EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_CHAR(tok, data) - @lex_state = EXPR_END - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_period(tok, data) - @lex_state = EXPR_DOT - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_int(tok, data) - @lex_state = EXPR_END | EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_float(tok, data) - @lex_state = EXPR_END | EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_rational(tok, data) - @lex_state = EXPR_END | EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_imaginary(tok, data) - @lex_state = EXPR_END | EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_symbeg(tok, data) - @lex_state = EXPR_FNAME - @continue = true - @in_fname = true - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - private def on_variables(event, tok, data) - if @in_fname - @lex_state = EXPR_ENDFN - @in_fname = false - @continue = false - elsif @continue - case @lex_state - when EXPR_DOT - @lex_state = EXPR_ARG - else - @lex_state = EXPR_ENDFN - @continue = false - end - else - @lex_state = EXPR_CMDARG - end - data << Token.new(lineno, column, event, tok, @lex_state) - end - - def on_ident(tok, data) - on_variables(__method__, tok, data) - end - - def on_ivar(tok, data) - @lex_state = EXPR_END - on_variables(__method__, tok, data) - end - - def on_cvar(tok, data) - @lex_state = EXPR_END - on_variables(__method__, tok, data) - end - - def on_gvar(tok, data) - @lex_state = EXPR_END - on_variables(__method__, tok, data) - end - - def on_backref(tok, data) - @lex_state = EXPR_END - on_variables(__method__, tok, data) - end - - def on_lparen(tok, data) - @lex_state = EXPR_LABEL | EXPR_BEG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_rparen(tok, data) - @lex_state = EXPR_ENDFN - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_lbrace(tok, data) - @lex_state = EXPR_LABEL | EXPR_BEG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_rbrace(tok, data) - @lex_state = EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_lbracket(tok, data) - @lex_state = EXPR_LABEL | EXPR_BEG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_rbracket(tok, data) - @lex_state = EXPR_ENDARG - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_const(tok, data) - case @lex_state - when EXPR_FNAME - @lex_state = EXPR_ENDFN - when EXPR_CLASS, EXPR_CMDARG, EXPR_MID - @lex_state = EXPR_ARG - else - @lex_state = EXPR_CMDARG - end - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_sp(tok, data) - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_comma(tok, data) - @lex_state = EXPR_BEG | EXPR_LABEL if (EXPR_ARG_ANY & @lex_state) != 0 - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_comment(tok, data) - @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0 - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_ignored_sp(tok, data) - @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0 - data << Token.new(lineno, column, __method__, tok, @lex_state) - end - - def on_heredoc_beg(tok, data) - data << Token.new(lineno, column, __method__, tok, @lex_state) - @lex_state = EXPR_END - data - end - - def on_heredoc_end(tok, data) - data << Token.new(lineno, column, __method__, tok, @lex_state) - @lex_state = EXPR_BEG - data - end - - def on_default(event, tok, data) - reset - data << Token.new(lineno, column, event, tok, @lex_state) - end - end unless RIPPER_HAS_LEX_STATE + EXPR_END = Ripper::EXPR_END + EXPR_ENDFN = Ripper::EXPR_ENDFN + EXPR_ARG = Ripper::EXPR_ARG + EXPR_FNAME = Ripper::EXPR_FNAME class InnerStateLex < Ripper::Filter def initialize(code) @@ -317,7 +22,7 @@ class RDoc::Parser::RipperStateLex def on_default(event, tok, data) data << Token.new(lineno, column, event, tok, state) end - end if RIPPER_HAS_LEX_STATE + end def get_squashed_tk if @buf.empty? @@ -333,9 +38,8 @@ class RDoc::Parser::RipperStateLex tk = get_string_tk(tk) when :on_backtick then if (tk[:state] & (EXPR_FNAME | EXPR_ENDFN)) != 0 - @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE tk[:kind] = :on_ident - tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG + tk[:state] = Ripper::Lexer::State.new(EXPR_ARG) else tk = get_string_tk(tk) end @@ -345,7 +49,6 @@ class RDoc::Parser::RipperStateLex tk = get_embdoc_tk(tk) when :on_heredoc_beg then @heredoc_queue << retrieve_heredoc_info(tk) - @inner_lex.lex_state = EXPR_END unless RIPPER_HAS_LEX_STATE when :on_nl, :on_ignored_nl, :on_comment, :on_heredoc_end then if !@heredoc_queue.empty? get_heredoc_tk(*@heredoc_queue.shift) @@ -549,8 +252,7 @@ class RDoc::Parser::RipperStateLex private def get_op_tk(tk) redefinable_operators = %w[! != !~ % & * ** + +@ - -@ / < << <= <=> == === =~ > >= >> [] []= ^ ` | ~] if redefinable_operators.include?(tk[:text]) and tk[:state] == EXPR_ARG then - @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE - tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG + tk[:state] = Ripper::Lexer::State.new(EXPR_ARG) tk[:kind] = :on_ident elsif tk[:text] =~ /^[-+]$/ then tk_ahead = get_squashed_tk diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index 85f1cd0391..909b02d2af 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -8,6 +8,15 @@ # by Keiju ISHITSUKA (Nippon Rational Inc.) # +if ENV['RDOC_USE_PRISM_PARSER'] + require 'rdoc/parser/prism_ruby' + RDoc::Parser.const_set(:Ruby, RDoc::Parser::PrismRuby) + puts "=========================================================================" + puts "RDoc is using the experimental Prism parser to generate the documentation" + puts "=========================================================================" + return +end + require 'ripper' require_relative 'ripper_state_lex' @@ -513,7 +522,7 @@ class RDoc::Parser::Ruby < RDoc::Parser when :on_comment, :on_embdoc then @read.pop if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and - (!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then + (!continue or (tk[:state] & Ripper::EXPR_LABEL) != 0) then break if !continue and nest <= 0 end when :on_comma then @@ -526,7 +535,7 @@ class RDoc::Parser::Ruby < RDoc::Parser nest += 1 when 'if', 'unless', 'while', 'until', 'rescue' # postfix if/unless/while/until/rescue must be EXPR_LABEL - nest += 1 unless (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0 + nest += 1 unless (tk[:state] & Ripper::EXPR_LABEL) != 0 when 'end' nest -= 1 break if nest == 0 @@ -1041,7 +1050,7 @@ class RDoc::Parser::Ruby < RDoc::Parser elsif (:on_kw == tk[:kind] && 'def' == tk[:text]) then nest += 1 elsif (:on_kw == tk[:kind] && %w{do if unless case begin}.include?(tk[:text])) then - if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0 + if (tk[:state] & Ripper::EXPR_LABEL) == 0 nest += 1 end elsif [:on_rparen, :on_rbrace, :on_rbracket].include?(tk[:kind]) || @@ -1662,7 +1671,7 @@ class RDoc::Parser::Ruby < RDoc::Parser when :on_comment, :on_embdoc then @read.pop if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and - (!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then + (!continue or (tk[:state] & Ripper::EXPR_LABEL) != 0) then if method && method.block_params.nil? then unget_tk tk read_documentation_modifiers method, modifiers @@ -1882,7 +1891,7 @@ class RDoc::Parser::Ruby < RDoc::Parser end when 'until', 'while' then - if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0 + if (tk[:state] & Ripper::EXPR_LABEL) == 0 nest += 1 skip_optional_do_after_expression end @@ -1898,7 +1907,7 @@ class RDoc::Parser::Ruby < RDoc::Parser skip_optional_do_after_expression when 'case', 'do', 'if', 'unless', 'begin' then - if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0 + if (tk[:state] & Ripper::EXPR_LABEL) == 0 nest += 1 end diff --git a/lib/rdoc/rd/block_parser.rb b/lib/rdoc/rd/block_parser.rb index 527147d91d..256ba553e5 100644 --- a/lib/rdoc/rd/block_parser.rb +++ b/lib/rdoc/rd/block_parser.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.7.3 +# This file is automatically generated by Racc 1.8.1 # from Racc grammar file "block_parser.ry". # @@ -23,7 +23,7 @@ unless $".find {|p| p.end_with?('/racc/info.rb')} $".push "#{__dir__}/racc/info.rb" module Racc - VERSION = '1.7.3' + VERSION = '1.8.1' Version = VERSION Copyright = 'Copyright (c) 1999-2006 Minero Aoki' end @@ -31,10 +31,6 @@ end end -unless defined?(NotImplementedError) - NotImplementedError = NotImplementError # :nodoc: -end - module Racc class ParseError < StandardError; end end @@ -42,7 +38,7 @@ unless defined?(::ParseError) ParseError = Racc::ParseError # :nodoc: end -# Racc is a LALR(1) parser generator. +# Racc is an LALR(1) parser generator. # It is written in Ruby itself, and generates Ruby programs. # # == Command-line Reference diff --git a/lib/rdoc/rd/inline_parser.rb b/lib/rdoc/rd/inline_parser.rb index adacf64d5b..b6d521c6bd 100644 --- a/lib/rdoc/rd/inline_parser.rb +++ b/lib/rdoc/rd/inline_parser.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.7.3 +# This file is automatically generated by Racc 1.8.1 # from Racc grammar file "inline_parser.ry". # @@ -23,7 +23,7 @@ unless $".find {|p| p.end_with?('/racc/info.rb')} $".push "#{__dir__}/racc/info.rb" module Racc - VERSION = '1.7.3' + VERSION = '1.8.1' Version = VERSION Copyright = 'Copyright (c) 1999-2006 Minero Aoki' end @@ -31,10 +31,6 @@ end end -unless defined?(NotImplementedError) - NotImplementedError = NotImplementError # :nodoc: -end - module Racc class ParseError < StandardError; end end @@ -42,7 +38,7 @@ unless defined?(::ParseError) ParseError = Racc::ParseError # :nodoc: end -# Racc is a LALR(1) parser generator. +# Racc is an LALR(1) parser generator. # It is written in Ruby itself, and generates Ruby programs. # # == Command-line Reference diff --git a/lib/rdoc/rdoc.gemspec b/lib/rdoc/rdoc.gemspec index 93a281c8ae..26f9ba1a87 100644 --- a/lib/rdoc/rdoc.gemspec +++ b/lib/rdoc/rdoc.gemspec @@ -46,27 +46,27 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "LEGAL.rdoc", "LICENSE.rdoc", "README.rdoc", - "RI.rdoc", + "RI.md", "TODO.rdoc", "exe/rdoc", "exe/ri", "lib/rdoc.rb", - "lib/rdoc/alias.rb", - "lib/rdoc/anon_class.rb", - "lib/rdoc/any_method.rb", - "lib/rdoc/attr.rb", - "lib/rdoc/class_module.rb", + "lib/rdoc/code_object/alias.rb", + "lib/rdoc/code_object/anon_class.rb", + "lib/rdoc/code_object/any_method.rb", + "lib/rdoc/code_object/attr.rb", + "lib/rdoc/code_object/class_module.rb", "lib/rdoc/code_object.rb", "lib/rdoc/code_objects.rb", "lib/rdoc/comment.rb", - "lib/rdoc/constant.rb", - "lib/rdoc/context.rb", - "lib/rdoc/context/section.rb", + "lib/rdoc/code_object/constant.rb", + "lib/rdoc/code_object/context.rb", + "lib/rdoc/code_object/context/section.rb", "lib/rdoc/cross_reference.rb", "lib/rdoc/encoding.rb", "lib/rdoc/erb_partial.rb", "lib/rdoc/erbio.rb", - "lib/rdoc/extend.rb", + "lib/rdoc/code_object/extend.rb", "lib/rdoc/generator.rb", "lib/rdoc/generator/darkfish.rb", "lib/rdoc/generator/json_index.rb", @@ -136,11 +136,11 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/generator/template/json_index/.document", "lib/rdoc/generator/template/json_index/js/navigation.js", "lib/rdoc/generator/template/json_index/js/searcher.js", - "lib/rdoc/ghost_method.rb", + "lib/rdoc/code_object/ghost_method.rb", "lib/rdoc/i18n.rb", "lib/rdoc/i18n/locale.rb", "lib/rdoc/i18n/text.rb", - "lib/rdoc/include.rb", + "lib/rdoc/code_object/include.rb", "lib/rdoc/known_classes.rb", "lib/rdoc/markdown.kpeg", "lib/rdoc/markdown/entities.rb", @@ -180,11 +180,11 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/markup/to_test.rb", "lib/rdoc/markup/to_tt_only.rb", "lib/rdoc/markup/verbatim.rb", - "lib/rdoc/meta_method.rb", - "lib/rdoc/method_attr.rb", - "lib/rdoc/mixin.rb", - "lib/rdoc/normal_class.rb", - "lib/rdoc/normal_module.rb", + "lib/rdoc/code_object/meta_method.rb", + "lib/rdoc/code_object/method_attr.rb", + "lib/rdoc/code_object/mixin.rb", + "lib/rdoc/code_object/normal_class.rb", + "lib/rdoc/code_object/normal_module.rb", "lib/rdoc/options.rb", "lib/rdoc/parser.rb", "lib/rdoc/parser/c.rb", @@ -201,7 +201,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/rd/inline.rb", "lib/rdoc/rd/inline_parser.ry", "lib/rdoc/rdoc.rb", - "lib/rdoc/require.rb", + "lib/rdoc/code_object/require.rb", "lib/rdoc/ri.rb", "lib/rdoc/ri/driver.rb", "lib/rdoc/ri/formatter.rb", @@ -210,7 +210,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/ri/task.rb", "lib/rdoc/rubygems_hook.rb", "lib/rdoc/servlet.rb", - "lib/rdoc/single_class.rb", + "lib/rdoc/code_object/single_class.rb", "lib/rdoc/stats.rb", "lib/rdoc/stats/normal.rb", "lib/rdoc/stats/quiet.rb", @@ -220,7 +220,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/text.rb", "lib/rdoc/token_stream.rb", "lib/rdoc/tom_doc.rb", - "lib/rdoc/top_level.rb", + "lib/rdoc/code_object/top_level.rb", "lib/rdoc/version.rb", "man/ri.1", ] diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 2da6d9b575..47108ceee3 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -356,7 +356,7 @@ option) top_level = @store.add_file filename, relative_name: relative_path.to_s - parser = RDoc::Parser.for top_level, filename, content, @options, @stats + parser = RDoc::Parser.for top_level, content, @options, @stats return unless parser @@ -544,7 +544,7 @@ end begin require 'rubygems' - rdoc_extensions = Gem.find_files 'rdoc/discover' + rdoc_extensions = Gem.find_latest_files 'rdoc/discover' rdoc_extensions.each do |extension| begin diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index 64783dc163..c6fddbac67 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -110,10 +110,6 @@ class RDoc::RI::Driver options = default_options opts = OptionParser.new do |opt| - opt.accept File do |file,| - File.readable?(file) and not File.directory?(file) and file - end - opt.program_name = File.basename $0 opt.version = RDoc::VERSION opt.release = nil @@ -345,9 +341,17 @@ or the PAGER environment variable. opt.separator nil - opt.on("--dump=CACHE", File, + opt.on("--dump=CACHE", "Dump data from an ri cache or data file.") do |value| - options[:dump_path] = value + unless File.readable?(value) + abort "#{value.inspect} is not readable" + end + + if File.directory?(value) + abort "#{value.inspect} is a directory" + end + + options[:dump_path] = File.new(value) end end diff --git a/lib/rdoc/task.rb b/lib/rdoc/task.rb index eb584c9d2a..ba697d0a93 100644 --- a/lib/rdoc/task.rb +++ b/lib/rdoc/task.rb @@ -104,9 +104,8 @@ class RDoc::Task < Rake::TaskLib attr_accessor :name ## - # Comment markup format. rdoc, rd and tomdoc are supported. (default is - # 'rdoc') - + # The markup format; one of: +rdoc+ (the default), +markdown+, +rd+, +tomdoc+. + # See {Markup Formats}[rdoc-ref:RDoc::Markup@Markup+Formats]. attr_accessor :markup ## diff --git a/lib/rdoc/tom_doc.rb b/lib/rdoc/tom_doc.rb index e161fcf42f..d10f024f70 100644 --- a/lib/rdoc/tom_doc.rb +++ b/lib/rdoc/tom_doc.rb @@ -3,13 +3,7 @@ # A parser for TomDoc based on TomDoc 1.0.0-rc1 (02adef9b5a) # -# The TomDoc specification can be found at: -# -# http://tomdoc.org -# -# The latest version of the TomDoc specification can be found at: -# -# https://github.com/mojombo/tomdoc/blob/master/tomdoc.md +# The TomDoc specification can be found at http://tomdoc.org. # # To choose TomDoc as your only default format see RDoc::Options@Saved+Options # for instructions on setting up a <code>.rdoc_options</code> file to store diff --git a/lib/rdoc/version.rb b/lib/rdoc/version.rb index 87842d9847..427d4ae232 100644 --- a/lib/rdoc/version.rb +++ b/lib/rdoc/version.rb @@ -5,6 +5,6 @@ module RDoc ## # RDoc version you are using - VERSION = '6.6.3.1' + VERSION = '6.7.0' end diff --git a/lib/reline.rb b/lib/reline.rb index fb00b96531..ddb0224180 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -7,6 +7,7 @@ require 'reline/key_stroke' require 'reline/line_editor' require 'reline/history' require 'reline/terminfo' +require 'reline/io' require 'reline/face' require 'rbconfig' @@ -18,20 +19,10 @@ module Reline class ConfigEncodingConversionError < StandardError; end Key = Struct.new(:char, :combined_char, :with_meta) do - def match?(other) - case other - when Reline::Key - (other.char.nil? or char.nil? or char == other.char) and - (other.combined_char.nil? or combined_char.nil? or combined_char == other.combined_char) and - (other.with_meta.nil? or with_meta.nil? or with_meta == other.with_meta) - when Integer, Symbol - (combined_char and combined_char == other) or - (combined_char.nil? and char and char == other) - else - false - end + # For dialog_proc `key.match?(dialog.name)` + def match?(sym) + combined_char.is_a?(Symbol) && combined_char == sym end - alias_method :==, :match? end CursorPos = Struct.new(:x, :y) DialogRenderInfo = Struct.new( @@ -263,7 +254,6 @@ module Reline raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') end - Reline.update_iogate io_gate.with_raw_input do inner_readline(prompt, add_hist, true, &confirm_multiline_termination) end @@ -286,7 +276,6 @@ module Reline def readline(prompt = '', add_hist = false) @mutex.synchronize do - Reline.update_iogate io_gate.with_raw_input do inner_readline(prompt, add_hist, false) end @@ -335,14 +324,17 @@ module Reline line_editor.prompt_proc = prompt_proc line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc + + # Readline calls pre_input_hook just after printing the first prompt. + line_editor.print_nomultiline_prompt pre_input_hook&.call - unless Reline::IOGate == Reline::GeneralIO + + unless Reline::IOGate.dumb? @dialog_proc_list.each_pair do |name_sym, d| line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context) end end - line_editor.print_nomultiline_prompt(prompt) line_editor.update_dialogs line_editor.rerender @@ -354,7 +346,7 @@ module Reline inputs.each do |key| if key.char == :bracketed_paste_start text = io_gate.read_bracketed_paste - line_editor.insert_pasted_text(text) + line_editor.insert_multiline_text(text) line_editor.scroll_into_view else line_editor.update(key) @@ -378,92 +370,39 @@ module Reline end end - # GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC - # is followed by a character, and times out and treats it as a standalone - # ESC if the second character does not arrive. If the second character - # comes before timed out, it is treated as a modifier key with the - # meta-property of meta-key, so that it can be distinguished from - # multibyte characters with the 8th bit turned on. - # - # GNU Readline will wait for the 2nd character with "keyseq-timeout" - # milli-seconds but wait forever after 3rd characters. + # GNU Readline watis for "keyseq-timeout" milliseconds when the input is + # ambiguous whether it is matching or matched. + # If the next character does not arrive within the specified timeout, input + # is considered as matched. + # `ESC` is ambiguous because it can be a standalone ESC (matched) or part of + # `ESC char` or part of CSI sequence (matching). private def read_io(keyseq_timeout, &block) buffer = [] + status = KeyStroke::MATCHING loop do - c = io_gate.getc(Float::INFINITY) - if c == -1 - result = :unmatched - else - buffer << c - result = key_stroke.match_status(buffer) - end - case result - when :matched - expanded = key_stroke.expand(buffer).map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } - block.(expanded) - break - when :matching - if buffer.size == 1 - case read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block) - when :break then break - when :next then next - end - end - when :unmatched - if buffer.size == 1 and c == "\e".ord - read_escaped_key(keyseq_timeout, c, block) + timeout = status == KeyStroke::MATCHING_MATCHED ? keyseq_timeout.fdiv(1000) : Float::INFINITY + c = io_gate.getc(timeout) + if c.nil? || c == -1 + if status == KeyStroke::MATCHING_MATCHED + status = KeyStroke::MATCHED + elsif buffer.empty? + # io_gate is closed and reached EOF + block.call([Key.new(nil, nil, false)]) + return else - expanded = buffer.map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } - block.(expanded) + status = KeyStroke::UNMATCHED end - break + else + buffer << c + status = key_stroke.match_status(buffer) end - end - end - private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block) - succ_c = io_gate.getc(keyseq_timeout.fdiv(1000)) - if succ_c - case key_stroke.match_status(buffer.dup.push(succ_c)) - when :unmatched - if c == "\e".ord - block.([Reline::Key.new(succ_c, succ_c | 0b10000000, true)]) - else - block.([Reline::Key.new(c, c, false), Reline::Key.new(succ_c, succ_c, false)]) - end - return :break - when :matching - io_gate.ungetc(succ_c) - return :next - when :matched - buffer << succ_c - expanded = key_stroke.expand(buffer).map{ |expanded_c| - Reline::Key.new(expanded_c, expanded_c, false) - } - block.(expanded) - return :break + if status == KeyStroke::MATCHED || status == KeyStroke::UNMATCHED + expanded, rest_bytes = key_stroke.expand(buffer) + rest_bytes.reverse_each { |c| io_gate.ungetc(c) } + block.call(expanded) + return end - else - block.([Reline::Key.new(c, c, false)]) - return :break - end - end - - private def read_escaped_key(keyseq_timeout, c, block) - escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000)) - - if escaped_c.nil? - block.([Reline::Key.new(c, c, false)]) - elsif escaped_c >= 128 # maybe, first byte of multi byte - block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)]) - elsif escaped_c == "\e".ord # escape twice - block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)]) - else - block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)]) end end @@ -473,7 +412,7 @@ module Reline end private def may_req_ambiguous_char_width - @ambiguous_width = 2 if io_gate == Reline::GeneralIO or !STDOUT.tty? + @ambiguous_width = 2 if io_gate.dumb? || !STDIN.tty? || !STDOUT.tty? return if defined? @ambiguous_width io_gate.move_cursor_column(0) begin @@ -521,8 +460,8 @@ module Reline def_single_delegator :line_editor, :byte_pointer, :point def_single_delegator :line_editor, :byte_pointer=, :point= - def self.insert_text(*args, &block) - line_editor.insert_text(*args, &block) + def self.insert_text(text) + line_editor.insert_multiline_text(text) self end @@ -567,37 +506,13 @@ module Reline def self.line_editor core.line_editor end +end - def self.update_iogate - return if core.config.test_mode - # Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen` - # Example: rails/spring boot the application in non-tty, then run console in tty. - if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty? - require 'reline/ansi' - remove_const(:IOGate) - const_set(:IOGate, Reline::ANSI) - end - end -end +Reline::IOGate = Reline::IO.decide_io_gate -require 'reline/general_io' -io = Reline::GeneralIO -unless ENV['TERM'] == 'dumb' - case RbConfig::CONFIG['host_os'] - when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ - require 'reline/windows' - tty = (io = Reline::Windows).msys_tty? - else - tty = $stdout.tty? - end -end -Reline::IOGate = if tty - require 'reline/ansi' - Reline::ANSI -else - io -end +# Deprecated +Reline::GeneralIO = Reline::Dumb.new Reline::Face.load_initial_configs diff --git a/lib/reline/config.rb b/lib/reline/config.rb index d44c2675ab..6aa6ba8d94 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -29,18 +29,31 @@ class Reline::Config attr_accessor :autocompletion def initialize - @additional_key_bindings = {} # from inputrc - @additional_key_bindings[:emacs] = {} - @additional_key_bindings[:vi_insert] = {} - @additional_key_bindings[:vi_command] = {} - @oneshot_key_bindings = {} + reset_variables + end + + def reset + if editing_mode_is?(:vi_command) + @editing_mode_label = :vi_insert + end + @oneshot_key_bindings.clear + end + + def reset_variables + @additional_key_bindings = { # from inputrc + emacs: Reline::KeyActor::Base.new, + vi_insert: Reline::KeyActor::Base.new, + vi_command: Reline::KeyActor::Base.new + } + @oneshot_key_bindings = Reline::KeyActor::Base.new @editing_mode_label = :emacs @keymap_label = :emacs @keymap_prefix = [] - @key_actors = {} - @key_actors[:emacs] = Reline::KeyActor::Emacs.new - @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new - @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new + @default_key_bindings = { + emacs: Reline::KeyActor::Base.new(Reline::KeyActor::EMACS_MAPPING), + vi_insert: Reline::KeyActor::Base.new(Reline::KeyActor::VI_INSERT_MAPPING), + vi_command: Reline::KeyActor::Base.new(Reline::KeyActor::VI_COMMAND_MAPPING) + } @vi_cmd_mode_string = '(cmd)' @vi_ins_mode_string = '(ins)' @emacs_mode_string = '@' @@ -49,20 +62,15 @@ class Reline::Config @keyseq_timeout = 500 @test_mode = false @autocompletion = false - @convert_meta = true if seven_bit_encoding?(Reline::IOGate.encoding) + @convert_meta = seven_bit_encoding?(Reline::IOGate.encoding) @loaded = false @enable_bracketed_paste = true - end - - def reset - if editing_mode_is?(:vi_command) - @editing_mode_label = :vi_insert - end - @oneshot_key_bindings.clear + @show_mode_in_prompt = false + @default_inputrc_path = nil end def editing_mode - @key_actors[@editing_mode_label] + @default_key_bindings[@editing_mode_label] end def editing_mode=(val) @@ -74,7 +82,7 @@ class Reline::Config end def keymap - @key_actors[@keymap_label] + @default_key_bindings[@keymap_label] end def loaded? @@ -133,14 +141,14 @@ class Reline::Config def key_bindings # The key bindings for each editing mode will be overwritten by the user-defined ones. - kb = @key_actors[@editing_mode_label].default_key_bindings.dup - kb.merge!(@additional_key_bindings[@editing_mode_label]) - kb.merge!(@oneshot_key_bindings) - kb + Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @default_key_bindings[@editing_mode_label]]) end def add_oneshot_key_binding(keystroke, target) - @oneshot_key_bindings[keystroke] = target + # IRB sets invalid keystroke [Reline::Key]. We should ignore it. + return unless keystroke.all? { |c| c.is_a?(Integer) } + + @oneshot_key_bindings.add(keystroke, target) end def reset_oneshot_key_bindings @@ -148,11 +156,11 @@ class Reline::Config end def add_default_key_binding_by_keymap(keymap, keystroke, target) - @key_actors[keymap].default_key_bindings[keystroke] = target + @default_key_bindings[keymap].add(keystroke, target) end def add_default_key_binding(keystroke, target) - @key_actors[@keymap_label].default_key_bindings[keystroke] = target + add_default_key_binding_by_keymap(@keymap_label, keystroke, target) end def read_lines(lines, file = nil) @@ -182,16 +190,17 @@ class Reline::Config next if if_stack.any? { |_no, skip| skip } case line - when /^set +([^ ]+) +([^ ]+)/i - var, value = $1.downcase, $2 - bind_variable(var, value) + when /^set +([^ ]+) +(.+)/i + # value ignores everything after a space, raw_value does not. + var, value, raw_value = $1.downcase, $2.partition(' ').first, $2 + bind_variable(var, value, raw_value) next when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o key, func_name = $1, $2 func_name = func_name.split.first keystroke, func = bind_key(key, func_name) next unless keystroke - @additional_key_bindings[@keymap_label][@keymap_prefix + keystroke] = func + @additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func) end end unless if_stack.empty? @@ -234,7 +243,7 @@ class Reline::Config end end - def bind_variable(name, value) + def bind_variable(name, value, raw_value) case name when 'history-size' begin @@ -242,24 +251,8 @@ class Reline::Config rescue ArgumentError @history_size = 500 end - when 'bell-style' - @bell_style = - case value - when 'none', 'off' - :none - when 'audible', 'on' - :audible - when 'visible' - :visible - else - :audible - end - when 'comment-begin' - @comment_begin = value.dup - when 'completion-query-items' - @completion_query_items = value.to_i when 'isearch-terminators' - @isearch_terminators = retrieve_string(value) + @isearch_terminators = retrieve_string(raw_value) when 'editing-mode' case value when 'emacs' @@ -301,11 +294,11 @@ class Reline::Config @show_mode_in_prompt = false end when 'vi-cmd-mode-string' - @vi_cmd_mode_string = retrieve_string(value) + @vi_cmd_mode_string = retrieve_string(raw_value) when 'vi-ins-mode-string' - @vi_ins_mode_string = retrieve_string(value) + @vi_ins_mode_string = retrieve_string(raw_value) when 'emacs-mode-string' - @emacs_mode_string = retrieve_string(value) + @emacs_mode_string = retrieve_string(raw_value) when *VARIABLE_NAMES then variable_name = :"@#{name.tr(?-, ?_)}" instance_variable_set(variable_name, value.nil? || value == '1' || value == 'on') @@ -373,6 +366,11 @@ class Reline::Config ret end + def reload + reset_variables + read + end + private def seven_bit_encoding?(encoding) encoding == Encoding::US_ASCII end diff --git a/lib/reline/face.rb b/lib/reline/face.rb index d07196e2e7..5b4464a623 100644 --- a/lib/reline/face.rb +++ b/lib/reline/face.rb @@ -107,7 +107,7 @@ class Reline::Face def sgr_rgb_256color(key, value) # 256 colors are - # 0..15: standard colors, hight intensity colors + # 0..15: standard colors, high intensity colors # 16..232: 216 colors (R, G, B each 6 steps) # 233..255: grayscale colors (24 steps) # This methods converts rgb_expression to 216 colors diff --git a/lib/reline/general_io.rb b/lib/reline/general_io.rb deleted file mode 100644 index d52151ad3c..0000000000 --- a/lib/reline/general_io.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'io/wait' - -class Reline::GeneralIO - RESET_COLOR = '' # Do not send color reset sequence - - def self.reset(encoding: nil) - @@pasting = false - if encoding - @@encoding = encoding - elsif defined?(@@encoding) - remove_class_variable(:@@encoding) - end - end - - def self.encoding - if defined?(@@encoding) - @@encoding - elsif RUBY_PLATFORM =~ /mswin|mingw/ - Encoding::UTF_8 - else - Encoding::default_external - end - end - - def self.win? - false - end - - def self.set_default_key_bindings(_) - end - - @@buf = [] - @@input = STDIN - - def self.input=(val) - @@input = val - end - - def self.with_raw_input - yield - end - - def self.getc(_timeout_second) - unless @@buf.empty? - return @@buf.shift - end - c = nil - loop do - Reline.core.line_editor.handle_signal - result = @@input.wait_readable(0.1) - next if result.nil? - c = @@input.read(1) - break - end - c&.ord - end - - def self.ungetc(c) - @@buf.unshift(c) - end - - def self.get_screen_size - [24, 80] - end - - def self.cursor_pos - Reline::CursorPos.new(1, 1) - end - - def self.hide_cursor - end - - def self.show_cursor - end - - def self.move_cursor_column(val) - end - - def self.move_cursor_up(val) - end - - def self.move_cursor_down(val) - end - - def self.erase_after_cursor - end - - def self.scroll_down(val) - end - - def self.clear_screen - end - - def self.set_screen_size(rows, columns) - end - - def self.set_winch_handler(&handler) - end - - @@pasting = false - - def self.in_pasting? - @@pasting - end - - def self.prep - end - - def self.deprep(otio) - end -end diff --git a/lib/reline/io.rb b/lib/reline/io.rb new file mode 100644 index 0000000000..c1dd1a56c8 --- /dev/null +++ b/lib/reline/io.rb @@ -0,0 +1,41 @@ + +module Reline + class IO + RESET_COLOR = "\e[0m" + + def self.decide_io_gate + if ENV['TERM'] == 'dumb' + Reline::Dumb.new + else + require 'reline/io/ansi' + + case RbConfig::CONFIG['host_os'] + when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ + require 'reline/io/windows' + io = Reline::Windows.new + if io.msys_tty? + Reline::ANSI.new + else + io + end + else + Reline::ANSI.new + end + end + end + + def dumb? + false + end + + def win? + false + end + + def reset_color_sequence + self.class::RESET_COLOR + end + end +end + +require 'reline/io/dumb' diff --git a/lib/reline/ansi.rb b/lib/reline/io/ansi.rb index 45a475a787..a730a953f7 100644 --- a/lib/reline/ansi.rb +++ b/lib/reline/io/ansi.rb @@ -1,10 +1,7 @@ require 'io/console' require 'io/wait' -require_relative 'terminfo' - -class Reline::ANSI - RESET_COLOR = "\e[0m" +class Reline::ANSI < Reline::IO CAPNAME_KEY_BINDINGS = { 'khome' => :ed_move_to_beg, 'kend' => :ed_move_to_end, @@ -36,15 +33,18 @@ class Reline::ANSI Reline::Terminfo.setupterm(0, 2) end - def self.encoding - Encoding.default_external + def initialize + @input = STDIN + @output = STDOUT + @buf = [] + @old_winch_handler = nil end - def self.win? - false + def encoding + Encoding.default_external end - def self.set_default_key_bindings(config, allow_terminfo: true) + def set_default_key_bindings(config, allow_terminfo: true) set_bracketed_paste_key_bindings(config) set_default_key_bindings_ansi_cursor(config) if allow_terminfo && Reline::Terminfo.enabled? @@ -67,13 +67,13 @@ class Reline::ANSI end end - def self.set_bracketed_paste_key_bindings(config) + def set_bracketed_paste_key_bindings(config) [:emacs, :vi_insert, :vi_command].each do |keymap| config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start) end end - def self.set_default_key_bindings_ansi_cursor(config) + def set_default_key_bindings_ansi_cursor(config) ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| bindings = [["\e[#{char}", default_func]] # CSI + char if modifiers[:ctrl] @@ -95,7 +95,7 @@ class Reline::ANSI end end - def self.set_default_key_bindings_terminfo(config) + def set_default_key_bindings_terminfo(config) key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding| begin key_code = Reline::Terminfo.tigetstr(capname) @@ -112,12 +112,16 @@ class Reline::ANSI end end - def self.set_default_key_bindings_comprehensive_list(config) + def set_default_key_bindings_comprehensive_list(config) { + # xterm + [27, 91, 51, 126] => :key_delete, # kdch1 + [27, 91, 53, 126] => :ed_search_prev_history, # kpp + [27, 91, 54, 126] => :ed_search_next_history, # knp + # Console (80x25) [27, 91, 49, 126] => :ed_move_to_beg, # Home [27, 91, 52, 126] => :ed_move_to_end, # End - [27, 91, 51, 126] => :key_delete, # Del # KDE # Del is 0x08 @@ -147,47 +151,42 @@ class Reline::ANSI end end - @@input = STDIN - def self.input=(val) - @@input = val + def input=(val) + @input = val end - @@output = STDOUT - def self.output=(val) - @@output = val + def output=(val) + @output = val end - def self.with_raw_input - if @@input.tty? - @@input.raw(intr: true) { yield } + def with_raw_input + if @input.tty? + @input.raw(intr: true) { yield } else yield end end - @@buf = [] - def self.inner_getc(timeout_second) - unless @@buf.empty? - return @@buf.shift + def inner_getc(timeout_second) + unless @buf.empty? + return @buf.shift end - until @@input.wait_readable(0.01) + until @input.wait_readable(0.01) timeout_second -= 0.01 return nil if timeout_second <= 0 Reline.core.line_editor.handle_signal end - c = @@input.getbyte - (c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c + c = @input.getbyte + (c == 0x16 && @input.tty? && @input.raw(min: 0, time: 0, &:getbyte)) || c rescue Errno::EIO # Maybe the I/O has been closed. nil - rescue Errno::ENOTTY - nil end START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT) END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT) - def self.read_bracketed_paste + def read_bracketed_paste buffer = String.new(encoding: Encoding::ASCII_8BIT) until buffer.end_with?(END_BRACKETED_PASTE) c = inner_getc(Float::INFINITY) @@ -199,60 +198,60 @@ class Reline::ANSI end # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second - def self.getc(timeout_second) + def getc(timeout_second) inner_getc(timeout_second) end - def self.in_pasting? + def in_pasting? not empty_buffer? end - def self.empty_buffer? - unless @@buf.empty? + def empty_buffer? + unless @buf.empty? return false end - !@@input.wait_readable(0) + !@input.wait_readable(0) end - def self.ungetc(c) - @@buf.unshift(c) + def ungetc(c) + @buf.unshift(c) end - def self.retrieve_keybuffer + def retrieve_keybuffer begin - return unless @@input.wait_readable(0.001) - str = @@input.read_nonblock(1024) + return unless @input.wait_readable(0.001) + str = @input.read_nonblock(1024) str.bytes.each do |c| - @@buf.push(c) + @buf.push(c) end rescue EOFError end end - def self.get_screen_size - s = @@input.winsize + def get_screen_size + s = @input.winsize return s if s[0] > 0 && s[1] > 0 s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i] return s if s[0] > 0 && s[1] > 0 [24, 80] - rescue Errno::ENOTTY + rescue Errno::ENOTTY, Errno::ENODEV [24, 80] end - def self.set_screen_size(rows, columns) - @@input.winsize = [rows, columns] + def set_screen_size(rows, columns) + @input.winsize = [rows, columns] self - rescue Errno::ENOTTY + rescue Errno::ENOTTY, Errno::ENODEV self end - def self.cursor_pos - begin + def cursor_pos + if both_tty? res = +'' m = nil - @@input.raw do |stdin| - @@output << "\e[6n" - @@output.flush + @input.raw do |stdin| + @output << "\e[6n" + @output.flush loop do c = stdin.getc next if c.nil? @@ -266,9 +265,9 @@ class Reline::ANSI end column = m[:column].to_i - 1 row = m[:row].to_i - 1 - rescue Errno::ENOTTY + else begin - buf = @@output.pread(@@output.pos, 0) + buf = @output.pread(@output.pos, 0) row = buf.count("\n") column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 rescue Errno::ESPIPE, IOError @@ -281,82 +280,93 @@ class Reline::ANSI Reline::CursorPos.new(column, row) end - def self.move_cursor_column(x) - @@output.write "\e[#{x + 1}G" + def both_tty? + @input.tty? && @output.tty? + end + + def move_cursor_column(x) + @output.write "\e[#{x + 1}G" end - def self.move_cursor_up(x) + def move_cursor_up(x) if x > 0 - @@output.write "\e[#{x}A" + @output.write "\e[#{x}A" elsif x < 0 move_cursor_down(-x) end end - def self.move_cursor_down(x) + def move_cursor_down(x) if x > 0 - @@output.write "\e[#{x}B" + @output.write "\e[#{x}B" elsif x < 0 move_cursor_up(-x) end end - def self.hide_cursor + def hide_cursor + seq = "\e[?25l" if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin - @@output.write Reline::Terminfo.tigetstr('civis') + seq = Reline::Terminfo.tigetstr('civis') rescue Reline::Terminfo::TerminfoError # civis is undefined end - else - # ignored end + @output.write seq end - def self.show_cursor + def show_cursor + seq = "\e[?25h" if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? begin - @@output.write Reline::Terminfo.tigetstr('cnorm') + seq = Reline::Terminfo.tigetstr('cnorm') rescue Reline::Terminfo::TerminfoError # cnorm is undefined end - else - # ignored end + @output.write seq end - def self.erase_after_cursor - @@output.write "\e[K" + def erase_after_cursor + @output.write "\e[K" end # This only works when the cursor is at the bottom of the scroll range # For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623 - def self.scroll_down(x) + def scroll_down(x) return if x.zero? # We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576 - @@output.write "\n" * x + @output.write "\n" * x end - def self.clear_screen - @@output.write "\e[2J" - @@output.write "\e[1;1H" + def clear_screen + @output.write "\e[2J" + @output.write "\e[1;1H" end - @@old_winch_handler = nil - def self.set_winch_handler(&handler) - @@old_winch_handler = Signal.trap('WINCH', &handler) + def set_winch_handler(&handler) + @old_winch_handler = Signal.trap('WINCH', &handler) + @old_cont_handler = Signal.trap('CONT') do + @input.raw!(intr: true) if @input.tty? + # Rerender the screen. Note that screen size might be changed while suspended. + handler.call + end + rescue ArgumentError + # Signal.trap may raise an ArgumentError if the platform doesn't support the signal. end - def self.prep + def prep # Enable bracketed paste - @@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste + @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty? retrieve_keybuffer nil end - def self.deprep(otio) + def deprep(otio) # Disable bracketed paste - @@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste - Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler + @output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty? + Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler + Signal.trap('CONT', @old_cont_handler) if @old_cont_handler end end diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb new file mode 100644 index 0000000000..6ed69ffdfa --- /dev/null +++ b/lib/reline/io/dumb.rb @@ -0,0 +1,106 @@ +require 'io/wait' + +class Reline::Dumb < Reline::IO + RESET_COLOR = '' # Do not send color reset sequence + + def initialize(encoding: nil) + @input = STDIN + @buf = [] + @pasting = false + @encoding = encoding + @screen_size = [24, 80] + end + + def dumb? + true + end + + def encoding + if @encoding + @encoding + elsif RUBY_PLATFORM =~ /mswin|mingw/ + Encoding::UTF_8 + else + Encoding::default_external + end + end + + def set_default_key_bindings(_) + end + + def input=(val) + @input = val + end + + def with_raw_input + yield + end + + def getc(_timeout_second) + unless @buf.empty? + return @buf.shift + end + c = nil + loop do + Reline.core.line_editor.handle_signal + result = @input.wait_readable(0.1) + next if result.nil? + c = @input.read(1) + break + end + c&.ord + end + + def ungetc(c) + @buf.unshift(c) + end + + def get_screen_size + @screen_size + end + + def cursor_pos + Reline::CursorPos.new(1, 1) + end + + def hide_cursor + end + + def show_cursor + end + + def move_cursor_column(val) + end + + def move_cursor_up(val) + end + + def move_cursor_down(val) + end + + def erase_after_cursor + end + + def scroll_down(val) + end + + def clear_screen + end + + def set_screen_size(rows, columns) + @screen_size = [rows, columns] + end + + def set_winch_handler(&handler) + end + + def in_pasting? + @pasting + end + + def prep + end + + def deprep(otio) + end +end diff --git a/lib/reline/windows.rb b/lib/reline/io/windows.rb index ee3f73e383..40025db504 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/io/windows.rb @@ -1,21 +1,49 @@ require 'fiddle/import' -class Reline::Windows - RESET_COLOR = "\e[0m" +class Reline::Windows < Reline::IO + def initialize + @input_buf = [] + @output_buf = [] + + @output = STDOUT + @hsg = nil + @getwch = Win32API.new('msvcrt', '_getwch', [], 'I') + @kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I') + @GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L') + @GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L') + @SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L') + @GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L') + @FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L') + @ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L') + @hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE) + @hConsoleInputHandle = @GetStdHandle.call(STD_INPUT_HANDLE) + @GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L') + @ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L') + @GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L') + @GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I') + @FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L') + @SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L') + + @GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L') + @SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L') + @WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L') + + @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 + end - def self.encoding + def encoding Encoding::UTF_8 end - def self.win? + def win? true end - def self.win_legacy_console? - @@legacy_console + def win_legacy_console? + @legacy_console end - def self.set_default_key_bindings(config) + def set_default_key_bindings(config) { [224, 72] => :ed_prev_history, # ↑ [224, 80] => :ed_next_history, # ↓ @@ -129,58 +157,42 @@ class Reline::Windows STD_OUTPUT_HANDLE = -11 FILE_TYPE_PIPE = 0x0003 FILE_NAME_INFO = 2 - @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I') - @@kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I') - @@GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L') - @@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L') - @@SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L') - @@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L') - @@FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L') - @@ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L') - @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE) - @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE) - @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L') - @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L') - @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L') - @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I') - @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L') - @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L') - - @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L') - @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L') - @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L') ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - private_class_method def self.getconsolemode + # Calling Win32API with console handle is reported to fail after executing some external command. + # We need to refresh console handle and retry the call again. + private def call_with_console_handle(win32func, *args) + val = win32func.call(@hConsoleHandle, *args) + return val if val != 0 + + @hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE) + win32func.call(@hConsoleHandle, *args) + end + + private def getconsolemode mode = "\000\000\000\000" - @@GetConsoleMode.call(@@hConsoleHandle, mode) + call_with_console_handle(@GetConsoleMode, mode) mode.unpack1('L') end - private_class_method def self.setconsolemode(mode) - @@SetConsoleMode.call(@@hConsoleHandle, mode) + private def setconsolemode(mode) + call_with_console_handle(@SetConsoleMode, mode) end - @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) - #if @@legacy_console + #if @legacy_console # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) - # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) + # @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) #end - @@input_buf = [] - @@output_buf = [] - - @@output = STDOUT - - def self.msys_tty?(io = @@hConsoleInputHandle) + def msys_tty?(io = @hConsoleInputHandle) # check if fd is a pipe - if @@GetFileType.call(io) != FILE_TYPE_PIPE + if @GetFileType.call(io) != FILE_TYPE_PIPE return false end bufsize = 1024 p_buffer = "\0" * bufsize - res = @@GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2) + res = @GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2) return false if res == 0 # get pipe name: p_buffer layout is: @@ -217,65 +229,63 @@ class Reline::Windows [ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ], ] - @@hsg = nil - - def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) + def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) # high-surrogate if 0xD800 <= char_code and char_code <= 0xDBFF - @@hsg = char_code + @hsg = char_code return end # low-surrogate if 0xDC00 <= char_code and char_code <= 0xDFFF - if @@hsg - char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00 - @@hsg = nil + if @hsg + char_code = 0x10000 + (@hsg - 0xD800) * 0x400 + char_code - 0xDC00 + @hsg = nil else # no high-surrogate. ignored. return end else # ignore high-surrogate without low-surrogate if there - @@hsg = nil + @hsg = nil end key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state) match = KEY_MAP.find { |args,| key.matches?(**args) } unless match.nil? - @@output_buf.concat(match.last) + @output_buf.concat(match.last) return end # no char, only control keys return if key.char_code == 0 and key.control_keys.any? - @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL) + @output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL) - @@output_buf.concat(key.char.bytes) + @output_buf.concat(key.char.bytes) end - def self.check_input_event + def check_input_event num_of_events = 0.chr * 8 - while @@output_buf.empty? + while @output_buf.empty? Reline.core.line_editor.handle_signal - if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec + if @WaitForSingleObject.(@hConsoleInputHandle, 100) != 0 # max 0.1 sec # prevent for background consolemode change - @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) + @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 next end - next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0 + next if @GetNumberOfConsoleInputEvents.(@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0 input_records = 0.chr * 20 * 80 read_event = 0.chr * 4 - if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_records, 80, read_event) != 0 + if @ReadConsoleInputW.(@hConsoleInputHandle, input_records, 80, read_event) != 0 read_events = read_event.unpack1('L') 0.upto(read_events) do |idx| input_record = input_records[idx * 20, 20] event = input_record[0, 2].unpack1('s*') case event when WINDOW_BUFFER_SIZE_EVENT - @@winch_handler.() + @winch_handler.() when KEY_EVENT key_down = input_record[4, 4].unpack1('l*') repeat_count = input_record[8, 2].unpack1('s*') @@ -293,34 +303,34 @@ class Reline::Windows end end - def self.with_raw_input + def with_raw_input yield end - def self.getc(_timeout_second) + def getc(_timeout_second) check_input_event - @@output_buf.shift + @output_buf.shift end - def self.ungetc(c) - @@output_buf.unshift(c) + def ungetc(c) + @output_buf.unshift(c) end - def self.in_pasting? - not self.empty_buffer? + def in_pasting? + not empty_buffer? end - def self.empty_buffer? - if not @@output_buf.empty? + def empty_buffer? + if not @output_buf.empty? false - elsif @@kbhit.call == 0 + elsif @kbhit.call == 0 true else false end end - def self.get_console_screen_buffer_info + def get_console_screen_buffer_info # CONSOLE_SCREEN_BUFFER_INFO # [ 0,2] dwSize.X # [ 2,2] dwSize.Y @@ -334,18 +344,18 @@ class Reline::Windows # [18,2] dwMaximumWindowSize.X # [20,2] dwMaximumWindowSize.Y csbi = 0.chr * 22 - return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0 + return if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) == 0 csbi end - def self.get_screen_size + def get_screen_size unless csbi = get_console_screen_buffer_info return [1, 1] end csbi[0, 4].unpack('SS').reverse end - def self.cursor_pos + def cursor_pos unless csbi = get_console_screen_buffer_info return Reline::CursorPos.new(0, 0) end @@ -354,49 +364,49 @@ class Reline::Windows Reline::CursorPos.new(x, y) end - def self.move_cursor_column(val) - @@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val) + def move_cursor_column(val) + call_with_console_handle(@SetConsoleCursorPosition, cursor_pos.y * 65536 + val) end - def self.move_cursor_up(val) + def move_cursor_up(val) if val > 0 y = cursor_pos.y - val y = 0 if y < 0 - @@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x) + call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + cursor_pos.x) elsif val < 0 move_cursor_down(-val) end end - def self.move_cursor_down(val) + def move_cursor_down(val) if val > 0 return unless csbi = get_console_screen_buffer_info screen_height = get_screen_size.first y = cursor_pos.y + val y = screen_height - 1 if y > (screen_height - 1) - @@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x) + call_with_console_handle(@SetConsoleCursorPosition, (cursor_pos.y + val) * 65536 + cursor_pos.x) elsif val < 0 move_cursor_up(-val) end end - def self.erase_after_cursor + def erase_after_cursor return unless csbi = get_console_screen_buffer_info attributes = csbi[8, 2].unpack1('S') cursor = csbi[4, 4].unpack1('L') written = 0.chr * 4 - @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written) - @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written) + call_with_console_handle(@FillConsoleOutputCharacter, 0x20, get_screen_size.last - cursor_pos.x, cursor, written) + call_with_console_handle(@FillConsoleOutputAttribute, attributes, get_screen_size.last - cursor_pos.x, cursor, written) end - def self.scroll_down(val) + def scroll_down(val) return if val < 0 return unless csbi = get_console_screen_buffer_info buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s') screen_height = window_bottom - window_top + 1 val = screen_height if val > screen_height - if @@legacy_console || window_left != 0 + if @legacy_console || window_left != 0 # unless ENABLE_VIRTUAL_TERMINAL, # if srWindow.Left != 0 then it's conhost.exe hosted console # and puts "\n" causes horizontal scroll. its glitch. @@ -404,11 +414,11 @@ class Reline::Windows scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4') destination_origin = 0 # y * 65536 + x fill = [' '.ord, attributes].pack('SS') - @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill) + call_with_console_handle(@ScrollConsoleScreenBuffer, scroll_rectangle, nil, destination_origin, fill) else origin_x = x + 1 origin_y = y - window_top + 1 - @@output.write [ + @output.write [ (origin_y != screen_height) ? "\e[#{screen_height};H" : nil, "\n" * val, (origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil @@ -416,49 +426,49 @@ class Reline::Windows end end - def self.clear_screen - if @@legacy_console + def clear_screen + if @legacy_console return unless csbi = get_console_screen_buffer_info buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s') fill_length = buffer_width * (window_bottom - window_top + 1) screen_topleft = window_top * 65536 written = 0.chr * 4 - @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written) - @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written) - @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft) + call_with_console_handle(@FillConsoleOutputCharacter, 0x20, fill_length, screen_topleft, written) + call_with_console_handle(@FillConsoleOutputAttribute, attributes, fill_length, screen_topleft, written) + call_with_console_handle(@SetConsoleCursorPosition, screen_topleft) else - @@output.write "\e[2J" "\e[H" + @output.write "\e[2J" "\e[H" end end - def self.set_screen_size(rows, columns) + def set_screen_size(rows, columns) raise NotImplementedError end - def self.hide_cursor + def hide_cursor size = 100 visible = 0 # 0 means false cursor_info = [size, visible].pack('Li') - @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) + call_with_console_handle(@SetConsoleCursorInfo, cursor_info) end - def self.show_cursor + def show_cursor size = 100 visible = 1 # 1 means true cursor_info = [size, visible].pack('Li') - @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) + call_with_console_handle(@SetConsoleCursorInfo, cursor_info) end - def self.set_winch_handler(&handler) - @@winch_handler = handler + def set_winch_handler(&handler) + @winch_handler = handler end - def self.prep + def prep # do nothing nil end - def self.deprep(otio) + def deprep(otio) # do nothing end diff --git a/lib/reline/key_actor.rb b/lib/reline/key_actor.rb index ebe09d2009..0ac7604556 100644 --- a/lib/reline/key_actor.rb +++ b/lib/reline/key_actor.rb @@ -2,6 +2,7 @@ module Reline::KeyActor end require 'reline/key_actor/base' +require 'reline/key_actor/composite' require 'reline/key_actor/emacs' require 'reline/key_actor/vi_command' require 'reline/key_actor/vi_insert' diff --git a/lib/reline/key_actor/base.rb b/lib/reline/key_actor/base.rb index 194e98938c..ee28c7681e 100644 --- a/lib/reline/key_actor/base.rb +++ b/lib/reline/key_actor/base.rb @@ -1,15 +1,31 @@ class Reline::KeyActor::Base - MAPPING = Array.new(256) + def initialize(mapping = []) + @mapping = mapping + @matching_bytes = {} + @key_bindings = {} + end def get_method(key) - self.class::MAPPING[key] + @mapping[key] + end + + def add(key, func) + (1...key.size).each do |size| + @matching_bytes[key.take(size)] = true + end + @key_bindings[key] = func + end + + def matching?(key) + @matching_bytes[key] end - def initialize - @default_key_bindings = {} + def get(key) + @key_bindings[key] end - def default_key_bindings - @default_key_bindings + def clear + @matching_bytes.clear + @key_bindings.clear end end diff --git a/lib/reline/key_actor/composite.rb b/lib/reline/key_actor/composite.rb new file mode 100644 index 0000000000..37e94ce6cf --- /dev/null +++ b/lib/reline/key_actor/composite.rb @@ -0,0 +1,17 @@ +class Reline::KeyActor::Composite + def initialize(key_actors) + @key_actors = key_actors + end + + def matching?(key) + @key_actors.any? { |key_actor| key_actor.matching?(key) } + end + + def get(key) + @key_actors.each do |key_actor| + func = key_actor.get(key) + return func if func + end + nil + end +end diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb index 9c797ba43e..ad84ee1d99 100644 --- a/lib/reline/key_actor/emacs.rb +++ b/lib/reline/key_actor/emacs.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::Emacs < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + EMACS_MAPPING = [ # 0 ^@ :em_set_mark, # 1 ^A @@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 30 ^^ :ed_unassigned, # 31 ^_ - :ed_unassigned, + :undo, # 32 SPACE :ed_insert, # 33 ! @@ -319,7 +319,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 158 M-^^ :ed_unassigned, # 159 M-^_ - :ed_unassigned, + :redo, # 160 M-SPACE :em_set_mark, # 161 M-! diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb index 06bb0ba8e4..d972c5e67f 100644 --- a/lib/reline/key_actor/vi_command.rb +++ b/lib/reline/key_actor/vi_command.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::ViCommand < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + VI_COMMAND_MAPPING = [ # 0 ^@ :ed_unassigned, # 1 ^A diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index f8ccf468c6..312df1646b 100644 --- a/lib/reline/key_actor/vi_insert.rb +++ b/lib/reline/key_actor/vi_insert.rb @@ -1,5 +1,5 @@ -class Reline::KeyActor::ViInsert < Reline::KeyActor::Base - MAPPING = [ +module Reline::KeyActor + VI_INSERT_MAPPING = [ # 0 ^@ :ed_unassigned, # 1 ^A diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb index bceffbb53f..ba40899685 100644 --- a/lib/reline/key_stroke.rb +++ b/lib/reline/key_stroke.rb @@ -7,138 +7,99 @@ class Reline::KeyStroke @config = config end - def compress_meta_key(ary) - return ary unless @config.convert_meta - ary.inject([]) { |result, key| - if result.size > 0 and result.last == "\e".ord - result[result.size - 1] = Reline::Key.new(key, key | 0b10000000, true) - else - result << key - end - result - } - end + # Input exactly matches to a key sequence + MATCHING = :matching + # Input partially matches to a key sequence + MATCHED = :matched + # Input matches to a key sequence and the key sequence is a prefix of another key sequence + MATCHING_MATCHED = :matching_matched + # Input does not match to any key sequence + UNMATCHED = :unmatched - def start_with?(me, other) - compressed_me = compress_meta_key(me) - compressed_other = compress_meta_key(other) - i = 0 - loop do - my_c = compressed_me[i] - other_c = compressed_other[i] - other_is_last = (i + 1) == compressed_other.size - me_is_last = (i + 1) == compressed_me.size - if my_c != other_c - if other_c == "\e".ord and other_is_last and my_c.is_a?(Reline::Key) and my_c.with_meta - return true - else - return false - end - elsif other_is_last - return true - elsif me_is_last - return false - end - i += 1 - end - end + def match_status(input) + matching = key_mapping.matching?(input) + matched = key_mapping.get(input) - def equal?(me, other) - case me - when Array - compressed_me = compress_meta_key(me) - compressed_other = compress_meta_key(other) - compressed_me.size == compressed_other.size and [compressed_me, compressed_other].transpose.all?{ |i| equal?(i[0], i[1]) } - when Integer - if other.is_a?(Reline::Key) - if other.combined_char == "\e".ord - false - else - other.combined_char == me - end - else - me == other - end - when Reline::Key - if other.is_a?(Integer) - me.combined_char == other - else - me == other - end - end - end + # FIXME: Workaround for single byte. remove this after MAPPING is merged into KeyActor. + matched ||= input.size == 1 + matching ||= input == [ESC_BYTE] - def match_status(input) - key_mapping.keys.select { |lhs| - start_with?(lhs, input) - }.tap { |it| - return :matched if it.size == 1 && equal?(it[0], input) - return :matching if it.size == 1 && !equal?(it[0], input) - return :matched if it.max_by(&:size)&.size&.< input.size - return :matching if it.size > 1 - } - if key_mapping.keys.any? { |lhs| start_with?(input, lhs) } - :matched + if matching && matched + MATCHING_MATCHED + elsif matching + MATCHING + elsif matched + MATCHED + elsif input[0] == ESC_BYTE + match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command)) + elsif input.size == 1 + MATCHED else - match_unknown_escape_sequence(input).first + UNMATCHED end end def expand(input) - lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last - unless lhs - status, size = match_unknown_escape_sequence(input) - case status - when :matched - return [:ed_unassigned] + expand(input.drop(size)) - when :matching - return [:ed_unassigned] - else - return input - end + matched_bytes = nil + (1..input.size).each do |i| + bytes = input.take(i) + status = match_status(bytes) + matched_bytes = bytes if status == MATCHED || status == MATCHING_MATCHED end - rhs = key_mapping[lhs] + return [[], []] unless matched_bytes - case rhs - when String - rhs_bytes = rhs.bytes - expand(expand(rhs_bytes) + expand(input.drop(lhs.size))) - when Symbol - [rhs] + expand(input.drop(lhs.size)) - when Array - rhs + func = key_mapping.get(matched_bytes) + if func.is_a?(Array) + keys = func.map { |c| Reline::Key.new(c, c, false) } + elsif func + keys = [Reline::Key.new(func, func, false)] + elsif matched_bytes.size == 1 + keys = [Reline::Key.new(matched_bytes.first, matched_bytes.first, false)] + elsif matched_bytes.size == 2 && matched_bytes[0] == ESC_BYTE + keys = [Reline::Key.new(matched_bytes[1], matched_bytes[1] | 0b10000000, true)] + else + keys = [] end + + [keys, input.drop(matched_bytes.size)] end private # returns match status of CSI/SS3 sequence and matched length - def match_unknown_escape_sequence(input) + def match_unknown_escape_sequence(input, vi_mode: false) idx = 0 - return [:unmatched, nil] unless input[idx] == ESC_BYTE + return UNMATCHED unless input[idx] == ESC_BYTE idx += 1 idx += 1 if input[idx] == ESC_BYTE case input[idx] when nil - return [:matching, nil] + if idx == 1 # `ESC` + return MATCHING_MATCHED + else # `ESC ESC` + return MATCHING + end when 91 # == '['.ord - # CSI sequence + # CSI sequence `ESC [ ... char` idx += 1 idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx]) idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx]) - input[idx] ? [:matched, idx + 1] : [:matching, nil] when 79 # == 'O'.ord - # SS3 sequence - input[idx + 1] ? [:matched, idx + 2] : [:matching, nil] + # SS3 sequence `ESC O char` + idx += 1 else - if idx == 1 - # `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence` - [:unmatched, nil] - else - # `ESC ESC char` - [:matched, idx + 1] - end + # `ESC char` or `ESC ESC char` + return UNMATCHED if vi_mode + end + + case input.size + when idx + MATCHING + when idx + 1 + MATCHED + else + UNMATCHED end end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 4c76932c10..c71a5f79ee 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -4,7 +4,6 @@ require 'reline/unicode' require 'tempfile' class Reline::LineEditor - # TODO: undo # TODO: Use "private alias_method" idiom after drop Ruby 2.5. attr_reader :byte_pointer attr_accessor :confirm_multiline_termination_proc @@ -46,6 +45,7 @@ class Reline::LineEditor RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true) CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer) + NullActionState = [nil, nil].freeze class MenuInfo attr_reader :list @@ -176,9 +176,8 @@ class Reline::LineEditor scroll_into_view Reline::IOGate.move_cursor_up @rendered_screen.cursor_y @rendered_screen.base_y = Reline::IOGate.cursor_pos.y - @rendered_screen.lines = [] - @rendered_screen.cursor_y = 0 - render_differential + clear_rendered_screen_cache + render end private def handle_interrupted @@ -186,11 +185,11 @@ class Reline::LineEditor @interrupted = false clear_dialogs - scrolldown = render_differential - Reline::IOGate.scroll_down scrolldown + render + cursor_to_bottom_offset = @rendered_screen.lines.size - @rendered_screen.cursor_y + Reline::IOGate.scroll_down cursor_to_bottom_offset Reline::IOGate.move_cursor_column 0 - @rendered_screen.lines = [] - @rendered_screen.cursor_y = 0 + clear_rendered_screen_cache case @old_trap when 'DEFAULT', 'SYSTEM_DEFAULT' raise Interrupt @@ -238,7 +237,6 @@ class Reline::LineEditor @perfect_matched = nil @menu_info = nil @searching_prompt = nil - @first_char = true @just_cursor_moving = false @eof = false @continuous_insertion_buffer = String.new(encoding: @encoding) @@ -251,6 +249,11 @@ class Reline::LineEditor @resized = false @cache = {} @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0) + @input_lines = [[[""], 0, 0]] + @input_lines_position = 0 + @undoing = false + @prev_action_state = NullActionState + @next_action_state = NullActionState reset_line end @@ -410,7 +413,7 @@ class Reline::LineEditor # do nothing elsif level == :blank Reline::IOGate.move_cursor_column base_x - @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" + @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}" else x, w, content = new_items[level] cover_begin = base_x != 0 && new_levels[base_x - 1] == level @@ -420,7 +423,7 @@ class Reline::LineEditor content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true) end Reline::IOGate.move_cursor_column x + pos - @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}" + @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}" end base_x += width end @@ -456,28 +459,7 @@ class Reline::LineEditor end def render_finished - clear_rendered_lines - render_full_content - end - - def clear_rendered_lines - Reline::IOGate.move_cursor_up @rendered_screen.cursor_y - Reline::IOGate.move_cursor_column 0 - - num_lines = @rendered_screen.lines.size - return unless num_lines && num_lines >= 1 - - Reline::IOGate.move_cursor_down num_lines - 1 - (num_lines - 1).times do - Reline::IOGate.erase_after_cursor - Reline::IOGate.move_cursor_up 1 - end - Reline::IOGate.erase_after_cursor - @rendered_screen.lines = [] - @rendered_screen.cursor_y = 0 - end - - def render_full_content + render_differential([], 0, 0) lines = @buffer_of_lines.size.times.map do |i| line = prompt_list[i] + modified_lines[i] wrapped_lines, = split_by_width(line, screen_width) @@ -486,19 +468,13 @@ class Reline::LineEditor @output.puts lines.map { |l| "#{l}\r\n" }.join end - def print_nomultiline_prompt(prompt) - return unless prompt && !@is_multiline - + def print_nomultiline_prompt # Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence. - @rendered_screen.lines = [[[0, Reline::Unicode.calculate_width(prompt, true), prompt]]] - @rendered_screen.cursor_y = 0 - @output.write prompt + @output.write @prompt if @prompt && !@is_multiline end - def render_differential + def render wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position - - rendered_lines = @rendered_screen.lines new_lines = wrapped_prompt_and_input_lines.flatten(1)[screen_scroll_top, screen_height].map do |prompt, line| prompt_width = Reline::Unicode.calculate_width(prompt, true) [[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]] @@ -516,12 +492,21 @@ class Reline::LineEditor x_range, y_range = dialog_range dialog, wrapped_cursor_y - screen_scroll_top y_range.each do |row| next if row < 0 || row >= screen_height + dialog_rows = new_lines[row] ||= [] # index 0 is for prompt, index 1 is for line, index 2.. is for dialog dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] end end + render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top + end + + # Reflects lines to be rendered and new cursor position to the screen + # by calculating the difference from the previous render. + + private def render_differential(new_lines, new_cursor_x, new_cursor_y) + rendered_lines = @rendered_screen.lines cursor_y = @rendered_screen.cursor_y if new_lines != rendered_lines # Hide cursor while rendering to avoid cursor flickering. @@ -548,11 +533,14 @@ class Reline::LineEditor @rendered_screen.lines = new_lines Reline::IOGate.show_cursor end - y = wrapped_cursor_y - screen_scroll_top - Reline::IOGate.move_cursor_column wrapped_cursor_x - Reline::IOGate.move_cursor_down y - cursor_y - @rendered_screen.cursor_y = y - new_lines.size - y + Reline::IOGate.move_cursor_column new_cursor_x + Reline::IOGate.move_cursor_down new_cursor_y - cursor_y + @rendered_screen.cursor_y = new_cursor_y + end + + private def clear_rendered_screen_cache + @rendered_screen.lines = [] + @rendered_screen.cursor_y = 0 end def upper_space_height(wrapped_cursor_y) @@ -564,7 +552,7 @@ class Reline::LineEditor end def rerender - render_differential unless @in_pasting + render unless @in_pasting end class DialogProcScope @@ -682,10 +670,8 @@ class Reline::LineEditor @trap_key.each do |t| @config.add_oneshot_key_binding(t, @name) end - elsif @trap_key.is_a?(Array) + else @config.add_oneshot_key_binding(@trap_key, @name) - elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key) - @config.add_oneshot_key_binding([@trap_key], @name) end end dialog_render_info @@ -948,7 +934,8 @@ class Reline::LineEditor unless @waiting_proc byte_pointer_diff = @byte_pointer - old_byte_pointer @byte_pointer = old_byte_pointer - send(@vi_waiting_operator, byte_pointer_diff) + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) cleanup_waiting end else @@ -1009,7 +996,8 @@ class Reline::LineEditor if @vi_waiting_operator byte_pointer_diff = @byte_pointer - old_byte_pointer @byte_pointer = old_byte_pointer - send(@vi_waiting_operator, byte_pointer_diff) + method_obj = method(@vi_waiting_operator) + wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff) cleanup_waiting end @kill_ring.process @@ -1076,17 +1064,7 @@ class Reline::LineEditor else # single byte return if key.char >= 128 # maybe, first byte of multi byte method_symbol = @config.editing_mode.get_method(key.combined_char) - if key.with_meta and method_symbol == :ed_unassigned - if @config.editing_mode_is?(:vi_command, :vi_insert) - # split ESC + key in vi mode - method_symbol = @config.editing_mode.get_method("\e".ord) - process_key("\e".ord, method_symbol) - method_symbol = @config.editing_mode.get_method(key.char) - process_key(key.char, method_symbol) - end - else - process_key(key.combined_char, method_symbol) - end + process_key(key.combined_char, method_symbol) @multibyte_buffer.clear end if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize @@ -1106,6 +1084,7 @@ class Reline::LineEditor end def input_key(key) + save_old_buffer @config.reset_oneshot_key_bindings @dialogs.each do |dialog| if key.char.instance_of?(Symbol) and key.char == dialog.name @@ -1114,14 +1093,10 @@ class Reline::LineEditor end if key.char.nil? process_insert(force: true) - if @first_char - @eof = true - end + @eof = buffer_empty? finish return end - old_lines = @buffer_of_lines.dup - @first_char = false @completion_occurs = false if key.char.is_a?(Symbol) @@ -1129,17 +1104,23 @@ class Reline::LineEditor else normal_char(key) end + + @prev_action_state, @next_action_state = @next_action_state, NullActionState + unless @completion_occurs @completion_state = CompletionState::NORMAL @completion_journey_state = nil end + push_input_lines unless @undoing + @undoing = false + if @in_pasting clear_dialogs return end - modified = old_lines != @buffer_of_lines + modified = @old_buffer_of_lines != @buffer_of_lines if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion # Auto complete starts only when edited process_insert(force: true) @@ -1148,6 +1129,29 @@ class Reline::LineEditor modified end + def save_old_buffer + @old_buffer_of_lines = @buffer_of_lines.dup + end + + def push_input_lines + if @old_buffer_of_lines == @buffer_of_lines + @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index] + else + @input_lines = @input_lines[0..@input_lines_position] + @input_lines_position += 1 + @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index]) + end + trim_input_lines + end + + MAX_INPUT_LINES = 100 + def trim_input_lines + if @input_lines.size > MAX_INPUT_LINES + @input_lines.shift + @input_lines_position -= 1 + end + end + def scroll_into_view _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position if wrapped_cursor_y < screen_scroll_top @@ -1224,6 +1228,18 @@ class Reline::LineEditor process_auto_indent end + def set_current_lines(lines, byte_pointer = nil, line_index = 0) + cursor = current_byte_pointer_cursor + @buffer_of_lines = lines + @line_index = line_index + if byte_pointer + @byte_pointer = byte_pointer + else + calculate_nearest_cursor(cursor) + end + process_auto_indent + end + def retrieve_completion_block(set_completion_quote_character = false) if Reline.completer_word_break_characters.empty? word_break_regexp = nil @@ -1305,7 +1321,8 @@ class Reline::LineEditor @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end - def insert_pasted_text(text) + def insert_multiline_text(text) + save_old_buffer pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer) post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..) lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1) @@ -1313,6 +1330,7 @@ class Reline::LineEditor @buffer_of_lines[@line_index, 1] = lines @line_index += lines.size - 1 @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize + push_input_lines end def insert_text(text) @@ -1371,6 +1389,10 @@ class Reline::LineEditor whole_lines.join("\n") end + private def buffer_empty? + current_line.empty? and @buffer_of_lines.size == 1 + end + def finished? @finished end @@ -1719,29 +1741,31 @@ class Reline::LineEditor end private def ed_search_prev_history(key, arg: 1) - substr = current_line.byteslice(0, @byte_pointer) + substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer) return if @history_pointer == 0 return if @history_pointer.nil? && substr.empty? && !current_line.empty? history_range = 0...(@history_pointer || Reline::HISTORY.size) h_pointer, line_index = search_history(substr, history_range.reverse_each) return unless h_pointer - move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) + move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer) arg -= 1 + set_next_action_state(:search_history, :empty) if substr.empty? ed_search_prev_history(key, arg: arg) if arg > 0 end alias_method :history_search_backward, :ed_search_prev_history private def ed_search_next_history(key, arg: 1) - substr = current_line.byteslice(0, @byte_pointer) + substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer) return if @history_pointer.nil? history_range = @history_pointer + 1...Reline::HISTORY.size h_pointer, line_index = search_history(substr, history_range) return if h_pointer.nil? and not substr.empty? - move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer) + move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer) arg -= 1 + set_next_action_state(:search_history, :empty) if substr.empty? ed_search_next_history(key, arg: arg) if arg > 0 end alias_method :history_search_forward, :ed_search_next_history @@ -1897,7 +1921,7 @@ class Reline::LineEditor alias_method :kill_whole_line, :em_kill_line private def em_delete(key) - if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord + if buffer_empty? and key == "\C-d".ord @eof = true finish elsif @byte_pointer < current_line.bytesize @@ -1942,9 +1966,8 @@ class Reline::LineEditor private def ed_clear_screen(key) Reline::IOGate.clear_screen @screen_size = Reline::IOGate.get_screen_size - @rendered_screen.lines = [] @rendered_screen.base_y = 0 - @rendered_screen.cursor_y = 0 + clear_rendered_screen_cache end alias_method :clear_screen, :ed_clear_screen @@ -2219,9 +2242,11 @@ class Reline::LineEditor line, cut = byteslice!(current_line, @byte_pointer, byte_pointer_diff) elsif byte_pointer_diff < 0 line, cut = byteslice!(current_line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff) + else + return end copy_for_vi(cut) - set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) + set_current_line(line, @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0)) end private def vi_yank(key, arg: nil) @@ -2240,13 +2265,14 @@ class Reline::LineEditor cut = current_line.byteslice(@byte_pointer, byte_pointer_diff) elsif byte_pointer_diff < 0 cut = current_line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff) + else + return end copy_for_vi(cut) end private def vi_list_or_eof(key) - if current_line.empty? and @buffer_of_lines.size == 1 - set_current_line('', 0) + if buffer_empty? @eof = true finish else @@ -2487,4 +2513,36 @@ class Reline::LineEditor private def vi_editing_mode(key) @config.editing_mode = :vi_insert end + + private def undo(_key) + @undoing = true + + return if @input_lines_position <= 0 + + @input_lines_position -= 1 + target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position] + set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y) + end + + private def redo(_key) + @undoing = true + + return if @input_lines_position >= @input_lines.size - 1 + + @input_lines_position += 1 + target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position] + set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y) + end + + private def prev_action_state_value(type) + @prev_action_state[0] == type ? @prev_action_state[1] : nil + end + + private def set_next_action_state(type, value) + @next_action_state = [type, value] + end + + private def re_read_init_file(_key) + @config.reload + end end diff --git a/lib/reline/terminfo.rb b/lib/reline/terminfo.rb index 6885a0c6be..c2b1f681b4 100644 --- a/lib/reline/terminfo.rb +++ b/lib/reline/terminfo.rb @@ -1,4 +1,7 @@ begin + # Ignore warning `Add fiddle to your Gemfile or gemspec` in Ruby 3.4. + # terminfo.rb and ansi.rb supports fiddle unavailable environment. + verbose, $VERBOSE = $VERBOSE, nil require 'fiddle' require 'fiddle/import' rescue LoadError @@ -7,6 +10,8 @@ rescue LoadError false end end +ensure + $VERBOSE = verbose end module Reline::Terminfo @@ -78,7 +83,7 @@ module Reline::Terminfo end def self.setupterm(term, fildes) - errret_int = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT) + errret_int = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT, Fiddle::RUBY_FREE) ret = @setupterm.(term, fildes, errret_int) case ret when 0 # OK diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index d7460d6d4a..ef239d5e9e 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -56,51 +56,26 @@ class Reline::Unicode require 'reline/unicode/east_asian_width' - HalfwidthDakutenHandakuten = /[\u{FF9E}\u{FF9F}]/ - - MBCharWidthRE = / - (?<width_2_1> - [#{ EscapedChars.map {|c| "\\x%02x" % c.ord }.join }] (?# ^ + char, such as ^M, ^H, ^[, ...) - ) - | (?<width_3>^\u{2E3B}) (?# THREE-EM DASH) - | (?<width_0>^\p{M}) - | (?<width_2_2> - #{ EastAsianWidth::TYPE_F } - | #{ EastAsianWidth::TYPE_W } - ) - | (?<width_1> - #{ EastAsianWidth::TYPE_H } - | #{ EastAsianWidth::TYPE_NA } - | #{ EastAsianWidth::TYPE_N } - )(?!#{ HalfwidthDakutenHandakuten }) - | (?<width_2_3> - (?: #{ EastAsianWidth::TYPE_H } - | #{ EastAsianWidth::TYPE_NA } - | #{ EastAsianWidth::TYPE_N }) - #{ HalfwidthDakutenHandakuten } - ) - | (?<ambiguous_width> - #{EastAsianWidth::TYPE_A} - ) - /x - def self.get_mbchar_width(mbchar) ord = mbchar.ord - if (0x00 <= ord and ord <= 0x1F) # in EscapedPairs + if ord <= 0x1F # in EscapedPairs return 2 - elsif (0x20 <= ord and ord <= 0x7E) # printable ASCII chars + elsif ord <= 0x7E # printable ASCII chars return 1 end - m = mbchar.encode(Encoding::UTF_8).match(MBCharWidthRE) - case - when m.nil? then 1 # TODO should be U+FFFD � REPLACEMENT CHARACTER - when m[:width_2_1], m[:width_2_2], m[:width_2_3] then 2 - when m[:width_3] then 3 - when m[:width_0] then 0 - when m[:width_1] then 1 - when m[:ambiguous_width] then Reline.ambiguous_width + utf8_mbchar = mbchar.encode(Encoding::UTF_8) + ord = utf8_mbchar.ord + chunk_index = EastAsianWidth::CHUNK_LAST.bsearch_index { |o| ord <= o } + size = EastAsianWidth::CHUNK_WIDTH[chunk_index] + if size == -1 + Reline.ambiguous_width + elsif size == 1 && utf8_mbchar.size >= 2 + second_char_ord = utf8_mbchar[1].ord + # Halfwidth Dakuten Handakuten + # Only these two character has Letter Modifier category and can be combined in a single grapheme cluster + (second_char_ord == 0xFF9E || second_char_ord == 0xFF9F) ? 2 : 1 else - nil + size end end diff --git a/lib/reline/unicode/east_asian_width.rb b/lib/reline/unicode/east_asian_width.rb index fa16a1bb56..106ca4881a 100644 --- a/lib/reline/unicode/east_asian_width.rb +++ b/lib/reline/unicode/east_asian_width.rb @@ -2,1195 +2,1266 @@ class Reline::Unicode::EastAsianWidth # This is based on EastAsianWidth.txt # UNICODE_VERSION = '15.1.0' - # Fullwidth - TYPE_F = /^[#{ %W( - \u{3000} - \u{FF01}-\u{FF60} - \u{FFE0}-\u{FFE6} - ).join }]/ - - # Halfwidth - TYPE_H = /^[#{ %W( - \u{20A9} - \u{FF61}-\u{FFBE} - \u{FFC2}-\u{FFC7} - \u{FFCA}-\u{FFCF} - \u{FFD2}-\u{FFD7} - \u{FFDA}-\u{FFDC} - \u{FFE8}-\u{FFEE} - ).join }]/ - - # Wide - TYPE_W = /^[#{ %W( - \u{1100}-\u{115F} - \u{231A}-\u{231B} - \u{2329}-\u{232A} - \u{23E9}-\u{23EC} - \u{23F0} - \u{23F3} - \u{25FD}-\u{25FE} - \u{2614}-\u{2615} - \u{2648}-\u{2653} - \u{267F} - \u{2693} - \u{26A1} - \u{26AA}-\u{26AB} - \u{26BD}-\u{26BE} - \u{26C4}-\u{26C5} - \u{26CE} - \u{26D4} - \u{26EA} - \u{26F2}-\u{26F3} - \u{26F5} - \u{26FA} - \u{26FD} - \u{2705} - \u{270A}-\u{270B} - \u{2728} - \u{274C} - \u{274E} - \u{2753}-\u{2755} - \u{2757} - \u{2795}-\u{2797} - \u{27B0} - \u{27BF} - \u{2B1B}-\u{2B1C} - \u{2B50} - \u{2B55} - \u{2E80}-\u{2E99} - \u{2E9B}-\u{2EF3} - \u{2F00}-\u{2FD5} - \u{2FF0}-\u{2FFF} - \u{3001}-\u{303E} - \u{3041}-\u{3096} - \u{3099}-\u{30FF} - \u{3105}-\u{312F} - \u{3131}-\u{318E} - \u{3190}-\u{31E3} - \u{31EF}-\u{321E} - \u{3220}-\u{3247} - \u{3250}-\u{4DBF} - \u{4E00}-\u{A48C} - \u{A490}-\u{A4C6} - \u{A960}-\u{A97C} - \u{AC00}-\u{D7A3} - \u{F900}-\u{FAFF} - \u{FE10}-\u{FE19} - \u{FE30}-\u{FE52} - \u{FE54}-\u{FE66} - \u{FE68}-\u{FE6B} - \u{16FE0}-\u{16FE4} - \u{16FF0}-\u{16FF1} - \u{17000}-\u{187F7} - \u{18800}-\u{18CD5} - \u{18D00}-\u{18D08} - \u{1AFF0}-\u{1AFF3} - \u{1AFF5}-\u{1AFFB} - \u{1AFFD}-\u{1AFFE} - \u{1B000}-\u{1B122} - \u{1B132} - \u{1B150}-\u{1B152} - \u{1B155} - \u{1B164}-\u{1B167} - \u{1B170}-\u{1B2FB} - \u{1F004} - \u{1F0CF} - \u{1F18E} - \u{1F191}-\u{1F19A} - \u{1F200}-\u{1F202} - \u{1F210}-\u{1F23B} - \u{1F240}-\u{1F248} - \u{1F250}-\u{1F251} - \u{1F260}-\u{1F265} - \u{1F300}-\u{1F320} - \u{1F32D}-\u{1F335} - \u{1F337}-\u{1F37C} - \u{1F37E}-\u{1F393} - \u{1F3A0}-\u{1F3CA} - \u{1F3CF}-\u{1F3D3} - \u{1F3E0}-\u{1F3F0} - \u{1F3F4} - \u{1F3F8}-\u{1F43E} - \u{1F440} - \u{1F442}-\u{1F4FC} - \u{1F4FF}-\u{1F53D} - \u{1F54B}-\u{1F54E} - \u{1F550}-\u{1F567} - \u{1F57A} - \u{1F595}-\u{1F596} - \u{1F5A4} - \u{1F5FB}-\u{1F64F} - \u{1F680}-\u{1F6C5} - \u{1F6CC} - \u{1F6D0}-\u{1F6D2} - \u{1F6D5}-\u{1F6D7} - \u{1F6DC}-\u{1F6DF} - \u{1F6EB}-\u{1F6EC} - \u{1F6F4}-\u{1F6FC} - \u{1F7E0}-\u{1F7EB} - \u{1F7F0} - \u{1F90C}-\u{1F93A} - \u{1F93C}-\u{1F945} - \u{1F947}-\u{1F9FF} - \u{1FA70}-\u{1FA7C} - \u{1FA80}-\u{1FA88} - \u{1FA90}-\u{1FABD} - \u{1FABF}-\u{1FAC5} - \u{1FACE}-\u{1FADB} - \u{1FAE0}-\u{1FAE8} - \u{1FAF0}-\u{1FAF8} - \u{20000}-\u{2FFFD} - \u{30000}-\u{3FFFD} - ).join }]/ - - # Narrow - TYPE_NA = /^[#{ %W( - \u{0020}-\u{007E} - \u{00A2}-\u{00A3} - \u{00A5}-\u{00A6} - \u{00AC} - \u{00AF} - \u{27E6}-\u{27ED} - \u{2985}-\u{2986} - ).join }]/ - - # Ambiguous - TYPE_A = /^[#{ %W( - \u{00A1} - \u{00A4} - \u{00A7}-\u{00A8} - \u{00AA} - \u{00AD}-\u{00AE} - \u{00B0}-\u{00B4} - \u{00B6}-\u{00BA} - \u{00BC}-\u{00BF} - \u{00C6} - \u{00D0} - \u{00D7}-\u{00D8} - \u{00DE}-\u{00E1} - \u{00E6} - \u{00E8}-\u{00EA} - \u{00EC}-\u{00ED} - \u{00F0} - \u{00F2}-\u{00F3} - \u{00F7}-\u{00FA} - \u{00FC} - \u{00FE} - \u{0101} - \u{0111} - \u{0113} - \u{011B} - \u{0126}-\u{0127} - \u{012B} - \u{0131}-\u{0133} - \u{0138} - \u{013F}-\u{0142} - \u{0144} - \u{0148}-\u{014B} - \u{014D} - \u{0152}-\u{0153} - \u{0166}-\u{0167} - \u{016B} - \u{01CE} - \u{01D0} - \u{01D2} - \u{01D4} - \u{01D6} - \u{01D8} - \u{01DA} - \u{01DC} - \u{0251} - \u{0261} - \u{02C4} - \u{02C7} - \u{02C9}-\u{02CB} - \u{02CD} - \u{02D0} - \u{02D8}-\u{02DB} - \u{02DD} - \u{02DF} - \u{0300}-\u{036F} - \u{0391}-\u{03A1} - \u{03A3}-\u{03A9} - \u{03B1}-\u{03C1} - \u{03C3}-\u{03C9} - \u{0401} - \u{0410}-\u{044F} - \u{0451} - \u{2010} - \u{2013}-\u{2016} - \u{2018}-\u{2019} - \u{201C}-\u{201D} - \u{2020}-\u{2022} - \u{2024}-\u{2027} - \u{2030} - \u{2032}-\u{2033} - \u{2035} - \u{203B} - \u{203E} - \u{2074} - \u{207F} - \u{2081}-\u{2084} - \u{20AC} - \u{2103} - \u{2105} - \u{2109} - \u{2113} - \u{2116} - \u{2121}-\u{2122} - \u{2126} - \u{212B} - \u{2153}-\u{2154} - \u{215B}-\u{215E} - \u{2160}-\u{216B} - \u{2170}-\u{2179} - \u{2189} - \u{2190}-\u{2199} - \u{21B8}-\u{21B9} - \u{21D2} - \u{21D4} - \u{21E7} - \u{2200} - \u{2202}-\u{2203} - \u{2207}-\u{2208} - \u{220B} - \u{220F} - \u{2211} - \u{2215} - \u{221A} - \u{221D}-\u{2220} - \u{2223} - \u{2225} - \u{2227}-\u{222C} - \u{222E} - \u{2234}-\u{2237} - \u{223C}-\u{223D} - \u{2248} - \u{224C} - \u{2252} - \u{2260}-\u{2261} - \u{2264}-\u{2267} - \u{226A}-\u{226B} - \u{226E}-\u{226F} - \u{2282}-\u{2283} - \u{2286}-\u{2287} - \u{2295} - \u{2299} - \u{22A5} - \u{22BF} - \u{2312} - \u{2460}-\u{24E9} - \u{24EB}-\u{254B} - \u{2550}-\u{2573} - \u{2580}-\u{258F} - \u{2592}-\u{2595} - \u{25A0}-\u{25A1} - \u{25A3}-\u{25A9} - \u{25B2}-\u{25B3} - \u{25B6}-\u{25B7} - \u{25BC}-\u{25BD} - \u{25C0}-\u{25C1} - \u{25C6}-\u{25C8} - \u{25CB} - \u{25CE}-\u{25D1} - \u{25E2}-\u{25E5} - \u{25EF} - \u{2605}-\u{2606} - \u{2609} - \u{260E}-\u{260F} - \u{261C} - \u{261E} - \u{2640} - \u{2642} - \u{2660}-\u{2661} - \u{2663}-\u{2665} - \u{2667}-\u{266A} - \u{266C}-\u{266D} - \u{266F} - \u{269E}-\u{269F} - \u{26BF} - \u{26C6}-\u{26CD} - \u{26CF}-\u{26D3} - \u{26D5}-\u{26E1} - \u{26E3} - \u{26E8}-\u{26E9} - \u{26EB}-\u{26F1} - \u{26F4} - \u{26F6}-\u{26F9} - \u{26FB}-\u{26FC} - \u{26FE}-\u{26FF} - \u{273D} - \u{2776}-\u{277F} - \u{2B56}-\u{2B59} - \u{3248}-\u{324F} - \u{E000}-\u{F8FF} - \u{FE00}-\u{FE0F} - \u{FFFD} - \u{1F100}-\u{1F10A} - \u{1F110}-\u{1F12D} - \u{1F130}-\u{1F169} - \u{1F170}-\u{1F18D} - \u{1F18F}-\u{1F190} - \u{1F19B}-\u{1F1AC} - \u{E0100}-\u{E01EF} - \u{F0000}-\u{FFFFD} - \u{100000}-\u{10FFFD} - ).join }]/ - - # Neutral - TYPE_N = /^[#{ %W( - \u{0000}-\u{001F} - \u{007F}-\u{00A0} - \u{00A9} - \u{00AB} - \u{00B5} - \u{00BB} - \u{00C0}-\u{00C5} - \u{00C7}-\u{00CF} - \u{00D1}-\u{00D6} - \u{00D9}-\u{00DD} - \u{00E2}-\u{00E5} - \u{00E7} - \u{00EB} - \u{00EE}-\u{00EF} - \u{00F1} - \u{00F4}-\u{00F6} - \u{00FB} - \u{00FD} - \u{00FF}-\u{0100} - \u{0102}-\u{0110} - \u{0112} - \u{0114}-\u{011A} - \u{011C}-\u{0125} - \u{0128}-\u{012A} - \u{012C}-\u{0130} - \u{0134}-\u{0137} - \u{0139}-\u{013E} - \u{0143} - \u{0145}-\u{0147} - \u{014C} - \u{014E}-\u{0151} - \u{0154}-\u{0165} - \u{0168}-\u{016A} - \u{016C}-\u{01CD} - \u{01CF} - \u{01D1} - \u{01D3} - \u{01D5} - \u{01D7} - \u{01D9} - \u{01DB} - \u{01DD}-\u{0250} - \u{0252}-\u{0260} - \u{0262}-\u{02C3} - \u{02C5}-\u{02C6} - \u{02C8} - \u{02CC} - \u{02CE}-\u{02CF} - \u{02D1}-\u{02D7} - \u{02DC} - \u{02DE} - \u{02E0}-\u{02FF} - \u{0370}-\u{0377} - \u{037A}-\u{037F} - \u{0384}-\u{038A} - \u{038C} - \u{038E}-\u{0390} - \u{03AA}-\u{03B0} - \u{03C2} - \u{03CA}-\u{0400} - \u{0402}-\u{040F} - \u{0450} - \u{0452}-\u{052F} - \u{0531}-\u{0556} - \u{0559}-\u{058A} - \u{058D}-\u{058F} - \u{0591}-\u{05C7} - \u{05D0}-\u{05EA} - \u{05EF}-\u{05F4} - \u{0600}-\u{070D} - \u{070F}-\u{074A} - \u{074D}-\u{07B1} - \u{07C0}-\u{07FA} - \u{07FD}-\u{082D} - \u{0830}-\u{083E} - \u{0840}-\u{085B} - \u{085E} - \u{0860}-\u{086A} - \u{0870}-\u{088E} - \u{0890}-\u{0891} - \u{0898}-\u{0983} - \u{0985}-\u{098C} - \u{098F}-\u{0990} - \u{0993}-\u{09A8} - \u{09AA}-\u{09B0} - \u{09B2} - \u{09B6}-\u{09B9} - \u{09BC}-\u{09C4} - \u{09C7}-\u{09C8} - \u{09CB}-\u{09CE} - \u{09D7} - \u{09DC}-\u{09DD} - \u{09DF}-\u{09E3} - \u{09E6}-\u{09FE} - \u{0A01}-\u{0A03} - \u{0A05}-\u{0A0A} - \u{0A0F}-\u{0A10} - \u{0A13}-\u{0A28} - \u{0A2A}-\u{0A30} - \u{0A32}-\u{0A33} - \u{0A35}-\u{0A36} - \u{0A38}-\u{0A39} - \u{0A3C} - \u{0A3E}-\u{0A42} - \u{0A47}-\u{0A48} - \u{0A4B}-\u{0A4D} - \u{0A51} - \u{0A59}-\u{0A5C} - \u{0A5E} - \u{0A66}-\u{0A76} - \u{0A81}-\u{0A83} - \u{0A85}-\u{0A8D} - \u{0A8F}-\u{0A91} - \u{0A93}-\u{0AA8} - \u{0AAA}-\u{0AB0} - \u{0AB2}-\u{0AB3} - \u{0AB5}-\u{0AB9} - \u{0ABC}-\u{0AC5} - \u{0AC7}-\u{0AC9} - \u{0ACB}-\u{0ACD} - \u{0AD0} - \u{0AE0}-\u{0AE3} - \u{0AE6}-\u{0AF1} - \u{0AF9}-\u{0AFF} - \u{0B01}-\u{0B03} - \u{0B05}-\u{0B0C} - \u{0B0F}-\u{0B10} - \u{0B13}-\u{0B28} - \u{0B2A}-\u{0B30} - \u{0B32}-\u{0B33} - \u{0B35}-\u{0B39} - \u{0B3C}-\u{0B44} - \u{0B47}-\u{0B48} - \u{0B4B}-\u{0B4D} - \u{0B55}-\u{0B57} - \u{0B5C}-\u{0B5D} - \u{0B5F}-\u{0B63} - \u{0B66}-\u{0B77} - \u{0B82}-\u{0B83} - \u{0B85}-\u{0B8A} - \u{0B8E}-\u{0B90} - \u{0B92}-\u{0B95} - \u{0B99}-\u{0B9A} - \u{0B9C} - \u{0B9E}-\u{0B9F} - \u{0BA3}-\u{0BA4} - \u{0BA8}-\u{0BAA} - \u{0BAE}-\u{0BB9} - \u{0BBE}-\u{0BC2} - \u{0BC6}-\u{0BC8} - \u{0BCA}-\u{0BCD} - \u{0BD0} - \u{0BD7} - \u{0BE6}-\u{0BFA} - \u{0C00}-\u{0C0C} - \u{0C0E}-\u{0C10} - \u{0C12}-\u{0C28} - \u{0C2A}-\u{0C39} - \u{0C3C}-\u{0C44} - \u{0C46}-\u{0C48} - \u{0C4A}-\u{0C4D} - \u{0C55}-\u{0C56} - \u{0C58}-\u{0C5A} - \u{0C5D} - \u{0C60}-\u{0C63} - \u{0C66}-\u{0C6F} - \u{0C77}-\u{0C8C} - \u{0C8E}-\u{0C90} - \u{0C92}-\u{0CA8} - \u{0CAA}-\u{0CB3} - \u{0CB5}-\u{0CB9} - \u{0CBC}-\u{0CC4} - \u{0CC6}-\u{0CC8} - \u{0CCA}-\u{0CCD} - \u{0CD5}-\u{0CD6} - \u{0CDD}-\u{0CDE} - \u{0CE0}-\u{0CE3} - \u{0CE6}-\u{0CEF} - \u{0CF1}-\u{0CF3} - \u{0D00}-\u{0D0C} - \u{0D0E}-\u{0D10} - \u{0D12}-\u{0D44} - \u{0D46}-\u{0D48} - \u{0D4A}-\u{0D4F} - \u{0D54}-\u{0D63} - \u{0D66}-\u{0D7F} - \u{0D81}-\u{0D83} - \u{0D85}-\u{0D96} - \u{0D9A}-\u{0DB1} - \u{0DB3}-\u{0DBB} - \u{0DBD} - \u{0DC0}-\u{0DC6} - \u{0DCA} - \u{0DCF}-\u{0DD4} - \u{0DD6} - \u{0DD8}-\u{0DDF} - \u{0DE6}-\u{0DEF} - \u{0DF2}-\u{0DF4} - \u{0E01}-\u{0E3A} - \u{0E3F}-\u{0E5B} - \u{0E81}-\u{0E82} - \u{0E84} - \u{0E86}-\u{0E8A} - \u{0E8C}-\u{0EA3} - \u{0EA5} - \u{0EA7}-\u{0EBD} - \u{0EC0}-\u{0EC4} - \u{0EC6} - \u{0EC8}-\u{0ECE} - \u{0ED0}-\u{0ED9} - \u{0EDC}-\u{0EDF} - \u{0F00}-\u{0F47} - \u{0F49}-\u{0F6C} - \u{0F71}-\u{0F97} - \u{0F99}-\u{0FBC} - \u{0FBE}-\u{0FCC} - \u{0FCE}-\u{0FDA} - \u{1000}-\u{10C5} - \u{10C7} - \u{10CD} - \u{10D0}-\u{10FF} - \u{1160}-\u{1248} - \u{124A}-\u{124D} - \u{1250}-\u{1256} - \u{1258} - \u{125A}-\u{125D} - \u{1260}-\u{1288} - \u{128A}-\u{128D} - \u{1290}-\u{12B0} - \u{12B2}-\u{12B5} - \u{12B8}-\u{12BE} - \u{12C0} - \u{12C2}-\u{12C5} - \u{12C8}-\u{12D6} - \u{12D8}-\u{1310} - \u{1312}-\u{1315} - \u{1318}-\u{135A} - \u{135D}-\u{137C} - \u{1380}-\u{1399} - \u{13A0}-\u{13F5} - \u{13F8}-\u{13FD} - \u{1400}-\u{169C} - \u{16A0}-\u{16F8} - \u{1700}-\u{1715} - \u{171F}-\u{1736} - \u{1740}-\u{1753} - \u{1760}-\u{176C} - \u{176E}-\u{1770} - \u{1772}-\u{1773} - \u{1780}-\u{17DD} - \u{17E0}-\u{17E9} - \u{17F0}-\u{17F9} - \u{1800}-\u{1819} - \u{1820}-\u{1878} - \u{1880}-\u{18AA} - \u{18B0}-\u{18F5} - \u{1900}-\u{191E} - \u{1920}-\u{192B} - \u{1930}-\u{193B} - \u{1940} - \u{1944}-\u{196D} - \u{1970}-\u{1974} - \u{1980}-\u{19AB} - \u{19B0}-\u{19C9} - \u{19D0}-\u{19DA} - \u{19DE}-\u{1A1B} - \u{1A1E}-\u{1A5E} - \u{1A60}-\u{1A7C} - \u{1A7F}-\u{1A89} - \u{1A90}-\u{1A99} - \u{1AA0}-\u{1AAD} - \u{1AB0}-\u{1ACE} - \u{1B00}-\u{1B4C} - \u{1B50}-\u{1B7E} - \u{1B80}-\u{1BF3} - \u{1BFC}-\u{1C37} - \u{1C3B}-\u{1C49} - \u{1C4D}-\u{1C88} - \u{1C90}-\u{1CBA} - \u{1CBD}-\u{1CC7} - \u{1CD0}-\u{1CFA} - \u{1D00}-\u{1F15} - \u{1F18}-\u{1F1D} - \u{1F20}-\u{1F45} - \u{1F48}-\u{1F4D} - \u{1F50}-\u{1F57} - \u{1F59} - \u{1F5B} - \u{1F5D} - \u{1F5F}-\u{1F7D} - \u{1F80}-\u{1FB4} - \u{1FB6}-\u{1FC4} - \u{1FC6}-\u{1FD3} - \u{1FD6}-\u{1FDB} - \u{1FDD}-\u{1FEF} - \u{1FF2}-\u{1FF4} - \u{1FF6}-\u{1FFE} - \u{2000}-\u{200F} - \u{2011}-\u{2012} - \u{2017} - \u{201A}-\u{201B} - \u{201E}-\u{201F} - \u{2023} - \u{2028}-\u{202F} - \u{2031} - \u{2034} - \u{2036}-\u{203A} - \u{203C}-\u{203D} - \u{203F}-\u{2064} - \u{2066}-\u{2071} - \u{2075}-\u{207E} - \u{2080} - \u{2085}-\u{208E} - \u{2090}-\u{209C} - \u{20A0}-\u{20A8} - \u{20AA}-\u{20AB} - \u{20AD}-\u{20C0} - \u{20D0}-\u{20F0} - \u{2100}-\u{2102} - \u{2104} - \u{2106}-\u{2108} - \u{210A}-\u{2112} - \u{2114}-\u{2115} - \u{2117}-\u{2120} - \u{2123}-\u{2125} - \u{2127}-\u{212A} - \u{212C}-\u{2152} - \u{2155}-\u{215A} - \u{215F} - \u{216C}-\u{216F} - \u{217A}-\u{2188} - \u{218A}-\u{218B} - \u{219A}-\u{21B7} - \u{21BA}-\u{21D1} - \u{21D3} - \u{21D5}-\u{21E6} - \u{21E8}-\u{21FF} - \u{2201} - \u{2204}-\u{2206} - \u{2209}-\u{220A} - \u{220C}-\u{220E} - \u{2210} - \u{2212}-\u{2214} - \u{2216}-\u{2219} - \u{221B}-\u{221C} - \u{2221}-\u{2222} - \u{2224} - \u{2226} - \u{222D} - \u{222F}-\u{2233} - \u{2238}-\u{223B} - \u{223E}-\u{2247} - \u{2249}-\u{224B} - \u{224D}-\u{2251} - \u{2253}-\u{225F} - \u{2262}-\u{2263} - \u{2268}-\u{2269} - \u{226C}-\u{226D} - \u{2270}-\u{2281} - \u{2284}-\u{2285} - \u{2288}-\u{2294} - \u{2296}-\u{2298} - \u{229A}-\u{22A4} - \u{22A6}-\u{22BE} - \u{22C0}-\u{2311} - \u{2313}-\u{2319} - \u{231C}-\u{2328} - \u{232B}-\u{23E8} - \u{23ED}-\u{23EF} - \u{23F1}-\u{23F2} - \u{23F4}-\u{2426} - \u{2440}-\u{244A} - \u{24EA} - \u{254C}-\u{254F} - \u{2574}-\u{257F} - \u{2590}-\u{2591} - \u{2596}-\u{259F} - \u{25A2} - \u{25AA}-\u{25B1} - \u{25B4}-\u{25B5} - \u{25B8}-\u{25BB} - \u{25BE}-\u{25BF} - \u{25C2}-\u{25C5} - \u{25C9}-\u{25CA} - \u{25CC}-\u{25CD} - \u{25D2}-\u{25E1} - \u{25E6}-\u{25EE} - \u{25F0}-\u{25FC} - \u{25FF}-\u{2604} - \u{2607}-\u{2608} - \u{260A}-\u{260D} - \u{2610}-\u{2613} - \u{2616}-\u{261B} - \u{261D} - \u{261F}-\u{263F} - \u{2641} - \u{2643}-\u{2647} - \u{2654}-\u{265F} - \u{2662} - \u{2666} - \u{266B} - \u{266E} - \u{2670}-\u{267E} - \u{2680}-\u{2692} - \u{2694}-\u{269D} - \u{26A0} - \u{26A2}-\u{26A9} - \u{26AC}-\u{26BC} - \u{26C0}-\u{26C3} - \u{26E2} - \u{26E4}-\u{26E7} - \u{2700}-\u{2704} - \u{2706}-\u{2709} - \u{270C}-\u{2727} - \u{2729}-\u{273C} - \u{273E}-\u{274B} - \u{274D} - \u{274F}-\u{2752} - \u{2756} - \u{2758}-\u{2775} - \u{2780}-\u{2794} - \u{2798}-\u{27AF} - \u{27B1}-\u{27BE} - \u{27C0}-\u{27E5} - \u{27EE}-\u{2984} - \u{2987}-\u{2B1A} - \u{2B1D}-\u{2B4F} - \u{2B51}-\u{2B54} - \u{2B5A}-\u{2B73} - \u{2B76}-\u{2B95} - \u{2B97}-\u{2CF3} - \u{2CF9}-\u{2D25} - \u{2D27} - \u{2D2D} - \u{2D30}-\u{2D67} - \u{2D6F}-\u{2D70} - \u{2D7F}-\u{2D96} - \u{2DA0}-\u{2DA6} - \u{2DA8}-\u{2DAE} - \u{2DB0}-\u{2DB6} - \u{2DB8}-\u{2DBE} - \u{2DC0}-\u{2DC6} - \u{2DC8}-\u{2DCE} - \u{2DD0}-\u{2DD6} - \u{2DD8}-\u{2DDE} - \u{2DE0}-\u{2E5D} - \u{303F} - \u{4DC0}-\u{4DFF} - \u{A4D0}-\u{A62B} - \u{A640}-\u{A6F7} - \u{A700}-\u{A7CA} - \u{A7D0}-\u{A7D1} - \u{A7D3} - \u{A7D5}-\u{A7D9} - \u{A7F2}-\u{A82C} - \u{A830}-\u{A839} - \u{A840}-\u{A877} - \u{A880}-\u{A8C5} - \u{A8CE}-\u{A8D9} - \u{A8E0}-\u{A953} - \u{A95F} - \u{A980}-\u{A9CD} - \u{A9CF}-\u{A9D9} - \u{A9DE}-\u{A9FE} - \u{AA00}-\u{AA36} - \u{AA40}-\u{AA4D} - \u{AA50}-\u{AA59} - \u{AA5C}-\u{AAC2} - \u{AADB}-\u{AAF6} - \u{AB01}-\u{AB06} - \u{AB09}-\u{AB0E} - \u{AB11}-\u{AB16} - \u{AB20}-\u{AB26} - \u{AB28}-\u{AB2E} - \u{AB30}-\u{AB6B} - \u{AB70}-\u{ABED} - \u{ABF0}-\u{ABF9} - \u{D7B0}-\u{D7C6} - \u{D7CB}-\u{D7FB} - \u{FB00}-\u{FB06} - \u{FB13}-\u{FB17} - \u{FB1D}-\u{FB36} - \u{FB38}-\u{FB3C} - \u{FB3E} - \u{FB40}-\u{FB41} - \u{FB43}-\u{FB44} - \u{FB46}-\u{FBC2} - \u{FBD3}-\u{FD8F} - \u{FD92}-\u{FDC7} - \u{FDCF} - \u{FDF0}-\u{FDFF} - \u{FE20}-\u{FE2F} - \u{FE70}-\u{FE74} - \u{FE76}-\u{FEFC} - \u{FEFF} - \u{FFF9}-\u{FFFC} - \u{10000}-\u{1000B} - \u{1000D}-\u{10026} - \u{10028}-\u{1003A} - \u{1003C}-\u{1003D} - \u{1003F}-\u{1004D} - \u{10050}-\u{1005D} - \u{10080}-\u{100FA} - \u{10100}-\u{10102} - \u{10107}-\u{10133} - \u{10137}-\u{1018E} - \u{10190}-\u{1019C} - \u{101A0} - \u{101D0}-\u{101FD} - \u{10280}-\u{1029C} - \u{102A0}-\u{102D0} - \u{102E0}-\u{102FB} - \u{10300}-\u{10323} - \u{1032D}-\u{1034A} - \u{10350}-\u{1037A} - \u{10380}-\u{1039D} - \u{1039F}-\u{103C3} - \u{103C8}-\u{103D5} - \u{10400}-\u{1049D} - \u{104A0}-\u{104A9} - \u{104B0}-\u{104D3} - \u{104D8}-\u{104FB} - \u{10500}-\u{10527} - \u{10530}-\u{10563} - \u{1056F}-\u{1057A} - \u{1057C}-\u{1058A} - \u{1058C}-\u{10592} - \u{10594}-\u{10595} - \u{10597}-\u{105A1} - \u{105A3}-\u{105B1} - \u{105B3}-\u{105B9} - \u{105BB}-\u{105BC} - \u{10600}-\u{10736} - \u{10740}-\u{10755} - \u{10760}-\u{10767} - \u{10780}-\u{10785} - \u{10787}-\u{107B0} - \u{107B2}-\u{107BA} - \u{10800}-\u{10805} - \u{10808} - \u{1080A}-\u{10835} - \u{10837}-\u{10838} - \u{1083C} - \u{1083F}-\u{10855} - \u{10857}-\u{1089E} - \u{108A7}-\u{108AF} - \u{108E0}-\u{108F2} - \u{108F4}-\u{108F5} - \u{108FB}-\u{1091B} - \u{1091F}-\u{10939} - \u{1093F} - \u{10980}-\u{109B7} - \u{109BC}-\u{109CF} - \u{109D2}-\u{10A03} - \u{10A05}-\u{10A06} - \u{10A0C}-\u{10A13} - \u{10A15}-\u{10A17} - \u{10A19}-\u{10A35} - \u{10A38}-\u{10A3A} - \u{10A3F}-\u{10A48} - \u{10A50}-\u{10A58} - \u{10A60}-\u{10A9F} - \u{10AC0}-\u{10AE6} - \u{10AEB}-\u{10AF6} - \u{10B00}-\u{10B35} - \u{10B39}-\u{10B55} - \u{10B58}-\u{10B72} - \u{10B78}-\u{10B91} - \u{10B99}-\u{10B9C} - \u{10BA9}-\u{10BAF} - \u{10C00}-\u{10C48} - \u{10C80}-\u{10CB2} - \u{10CC0}-\u{10CF2} - \u{10CFA}-\u{10D27} - \u{10D30}-\u{10D39} - \u{10E60}-\u{10E7E} - \u{10E80}-\u{10EA9} - \u{10EAB}-\u{10EAD} - \u{10EB0}-\u{10EB1} - \u{10EFD}-\u{10F27} - \u{10F30}-\u{10F59} - \u{10F70}-\u{10F89} - \u{10FB0}-\u{10FCB} - \u{10FE0}-\u{10FF6} - \u{11000}-\u{1104D} - \u{11052}-\u{11075} - \u{1107F}-\u{110C2} - \u{110CD} - \u{110D0}-\u{110E8} - \u{110F0}-\u{110F9} - \u{11100}-\u{11134} - \u{11136}-\u{11147} - \u{11150}-\u{11176} - \u{11180}-\u{111DF} - \u{111E1}-\u{111F4} - \u{11200}-\u{11211} - \u{11213}-\u{11241} - \u{11280}-\u{11286} - \u{11288} - \u{1128A}-\u{1128D} - \u{1128F}-\u{1129D} - \u{1129F}-\u{112A9} - \u{112B0}-\u{112EA} - \u{112F0}-\u{112F9} - \u{11300}-\u{11303} - \u{11305}-\u{1130C} - \u{1130F}-\u{11310} - \u{11313}-\u{11328} - \u{1132A}-\u{11330} - \u{11332}-\u{11333} - \u{11335}-\u{11339} - \u{1133B}-\u{11344} - \u{11347}-\u{11348} - \u{1134B}-\u{1134D} - \u{11350} - \u{11357} - \u{1135D}-\u{11363} - \u{11366}-\u{1136C} - \u{11370}-\u{11374} - \u{11400}-\u{1145B} - \u{1145D}-\u{11461} - \u{11480}-\u{114C7} - \u{114D0}-\u{114D9} - \u{11580}-\u{115B5} - \u{115B8}-\u{115DD} - \u{11600}-\u{11644} - \u{11650}-\u{11659} - \u{11660}-\u{1166C} - \u{11680}-\u{116B9} - \u{116C0}-\u{116C9} - \u{11700}-\u{1171A} - \u{1171D}-\u{1172B} - \u{11730}-\u{11746} - \u{11800}-\u{1183B} - \u{118A0}-\u{118F2} - \u{118FF}-\u{11906} - \u{11909} - \u{1190C}-\u{11913} - \u{11915}-\u{11916} - \u{11918}-\u{11935} - \u{11937}-\u{11938} - \u{1193B}-\u{11946} - \u{11950}-\u{11959} - \u{119A0}-\u{119A7} - \u{119AA}-\u{119D7} - \u{119DA}-\u{119E4} - \u{11A00}-\u{11A47} - \u{11A50}-\u{11AA2} - \u{11AB0}-\u{11AF8} - \u{11B00}-\u{11B09} - \u{11C00}-\u{11C08} - \u{11C0A}-\u{11C36} - \u{11C38}-\u{11C45} - \u{11C50}-\u{11C6C} - \u{11C70}-\u{11C8F} - \u{11C92}-\u{11CA7} - \u{11CA9}-\u{11CB6} - \u{11D00}-\u{11D06} - \u{11D08}-\u{11D09} - \u{11D0B}-\u{11D36} - \u{11D3A} - \u{11D3C}-\u{11D3D} - \u{11D3F}-\u{11D47} - \u{11D50}-\u{11D59} - \u{11D60}-\u{11D65} - \u{11D67}-\u{11D68} - \u{11D6A}-\u{11D8E} - \u{11D90}-\u{11D91} - \u{11D93}-\u{11D98} - \u{11DA0}-\u{11DA9} - \u{11EE0}-\u{11EF8} - \u{11F00}-\u{11F10} - \u{11F12}-\u{11F3A} - \u{11F3E}-\u{11F59} - \u{11FB0} - \u{11FC0}-\u{11FF1} - \u{11FFF}-\u{12399} - \u{12400}-\u{1246E} - \u{12470}-\u{12474} - \u{12480}-\u{12543} - \u{12F90}-\u{12FF2} - \u{13000}-\u{13455} - \u{14400}-\u{14646} - \u{16800}-\u{16A38} - \u{16A40}-\u{16A5E} - \u{16A60}-\u{16A69} - \u{16A6E}-\u{16ABE} - \u{16AC0}-\u{16AC9} - \u{16AD0}-\u{16AED} - \u{16AF0}-\u{16AF5} - \u{16B00}-\u{16B45} - \u{16B50}-\u{16B59} - \u{16B5B}-\u{16B61} - \u{16B63}-\u{16B77} - \u{16B7D}-\u{16B8F} - \u{16E40}-\u{16E9A} - \u{16F00}-\u{16F4A} - \u{16F4F}-\u{16F87} - \u{16F8F}-\u{16F9F} - \u{1BC00}-\u{1BC6A} - \u{1BC70}-\u{1BC7C} - \u{1BC80}-\u{1BC88} - \u{1BC90}-\u{1BC99} - \u{1BC9C}-\u{1BCA3} - \u{1CF00}-\u{1CF2D} - \u{1CF30}-\u{1CF46} - \u{1CF50}-\u{1CFC3} - \u{1D000}-\u{1D0F5} - \u{1D100}-\u{1D126} - \u{1D129}-\u{1D1EA} - \u{1D200}-\u{1D245} - \u{1D2C0}-\u{1D2D3} - \u{1D2E0}-\u{1D2F3} - \u{1D300}-\u{1D356} - \u{1D360}-\u{1D378} - \u{1D400}-\u{1D454} - \u{1D456}-\u{1D49C} - \u{1D49E}-\u{1D49F} - \u{1D4A2} - \u{1D4A5}-\u{1D4A6} - \u{1D4A9}-\u{1D4AC} - \u{1D4AE}-\u{1D4B9} - \u{1D4BB} - \u{1D4BD}-\u{1D4C3} - \u{1D4C5}-\u{1D505} - \u{1D507}-\u{1D50A} - \u{1D50D}-\u{1D514} - \u{1D516}-\u{1D51C} - \u{1D51E}-\u{1D539} - \u{1D53B}-\u{1D53E} - \u{1D540}-\u{1D544} - \u{1D546} - \u{1D54A}-\u{1D550} - \u{1D552}-\u{1D6A5} - \u{1D6A8}-\u{1D7CB} - \u{1D7CE}-\u{1DA8B} - \u{1DA9B}-\u{1DA9F} - \u{1DAA1}-\u{1DAAF} - \u{1DF00}-\u{1DF1E} - \u{1DF25}-\u{1DF2A} - \u{1E000}-\u{1E006} - \u{1E008}-\u{1E018} - \u{1E01B}-\u{1E021} - \u{1E023}-\u{1E024} - \u{1E026}-\u{1E02A} - \u{1E030}-\u{1E06D} - \u{1E08F} - \u{1E100}-\u{1E12C} - \u{1E130}-\u{1E13D} - \u{1E140}-\u{1E149} - \u{1E14E}-\u{1E14F} - \u{1E290}-\u{1E2AE} - \u{1E2C0}-\u{1E2F9} - \u{1E2FF} - \u{1E4D0}-\u{1E4F9} - \u{1E7E0}-\u{1E7E6} - \u{1E7E8}-\u{1E7EB} - \u{1E7ED}-\u{1E7EE} - \u{1E7F0}-\u{1E7FE} - \u{1E800}-\u{1E8C4} - \u{1E8C7}-\u{1E8D6} - \u{1E900}-\u{1E94B} - \u{1E950}-\u{1E959} - \u{1E95E}-\u{1E95F} - \u{1EC71}-\u{1ECB4} - \u{1ED01}-\u{1ED3D} - \u{1EE00}-\u{1EE03} - \u{1EE05}-\u{1EE1F} - \u{1EE21}-\u{1EE22} - \u{1EE24} - \u{1EE27} - \u{1EE29}-\u{1EE32} - \u{1EE34}-\u{1EE37} - \u{1EE39} - \u{1EE3B} - \u{1EE42} - \u{1EE47} - \u{1EE49} - \u{1EE4B} - \u{1EE4D}-\u{1EE4F} - \u{1EE51}-\u{1EE52} - \u{1EE54} - \u{1EE57} - \u{1EE59} - \u{1EE5B} - \u{1EE5D} - \u{1EE5F} - \u{1EE61}-\u{1EE62} - \u{1EE64} - \u{1EE67}-\u{1EE6A} - \u{1EE6C}-\u{1EE72} - \u{1EE74}-\u{1EE77} - \u{1EE79}-\u{1EE7C} - \u{1EE7E} - \u{1EE80}-\u{1EE89} - \u{1EE8B}-\u{1EE9B} - \u{1EEA1}-\u{1EEA3} - \u{1EEA5}-\u{1EEA9} - \u{1EEAB}-\u{1EEBB} - \u{1EEF0}-\u{1EEF1} - \u{1F000}-\u{1F003} - \u{1F005}-\u{1F02B} - \u{1F030}-\u{1F093} - \u{1F0A0}-\u{1F0AE} - \u{1F0B1}-\u{1F0BF} - \u{1F0C1}-\u{1F0CE} - \u{1F0D1}-\u{1F0F5} - \u{1F10B}-\u{1F10F} - \u{1F12E}-\u{1F12F} - \u{1F16A}-\u{1F16F} - \u{1F1AD} - \u{1F1E6}-\u{1F1FF} - \u{1F321}-\u{1F32C} - \u{1F336} - \u{1F37D} - \u{1F394}-\u{1F39F} - \u{1F3CB}-\u{1F3CE} - \u{1F3D4}-\u{1F3DF} - \u{1F3F1}-\u{1F3F3} - \u{1F3F5}-\u{1F3F7} - \u{1F43F} - \u{1F441} - \u{1F4FD}-\u{1F4FE} - \u{1F53E}-\u{1F54A} - \u{1F54F} - \u{1F568}-\u{1F579} - \u{1F57B}-\u{1F594} - \u{1F597}-\u{1F5A3} - \u{1F5A5}-\u{1F5FA} - \u{1F650}-\u{1F67F} - \u{1F6C6}-\u{1F6CB} - \u{1F6CD}-\u{1F6CF} - \u{1F6D3}-\u{1F6D4} - \u{1F6E0}-\u{1F6EA} - \u{1F6F0}-\u{1F6F3} - \u{1F700}-\u{1F776} - \u{1F77B}-\u{1F7D9} - \u{1F800}-\u{1F80B} - \u{1F810}-\u{1F847} - \u{1F850}-\u{1F859} - \u{1F860}-\u{1F887} - \u{1F890}-\u{1F8AD} - \u{1F8B0}-\u{1F8B1} - \u{1F900}-\u{1F90B} - \u{1F93B} - \u{1F946} - \u{1FA00}-\u{1FA53} - \u{1FA60}-\u{1FA6D} - \u{1FB00}-\u{1FB92} - \u{1FB94}-\u{1FBCA} - \u{1FBF0}-\u{1FBF9} - \u{E0001} - \u{E0020}-\u{E007F} - ).join }]/ + CHUNK_LAST, CHUNK_WIDTH = [ + [0x1f, 2], + [0x7e, 1], + [0x7f, 2], + [0xa0, 1], + [0xa1, -1], + [0xa3, 1], + [0xa4, -1], + [0xa6, 1], + [0xa8, -1], + [0xa9, 1], + [0xaa, -1], + [0xac, 1], + [0xae, -1], + [0xaf, 1], + [0xb4, -1], + [0xb5, 1], + [0xba, -1], + [0xbb, 1], + [0xbf, -1], + [0xc5, 1], + [0xc6, -1], + [0xcf, 1], + [0xd0, -1], + [0xd6, 1], + [0xd8, -1], + [0xdd, 1], + [0xe1, -1], + [0xe5, 1], + [0xe6, -1], + [0xe7, 1], + [0xea, -1], + [0xeb, 1], + [0xed, -1], + [0xef, 1], + [0xf0, -1], + [0xf1, 1], + [0xf3, -1], + [0xf6, 1], + [0xfa, -1], + [0xfb, 1], + [0xfc, -1], + [0xfd, 1], + [0xfe, -1], + [0x100, 1], + [0x101, -1], + [0x110, 1], + [0x111, -1], + [0x112, 1], + [0x113, -1], + [0x11a, 1], + [0x11b, -1], + [0x125, 1], + [0x127, -1], + [0x12a, 1], + [0x12b, -1], + [0x130, 1], + [0x133, -1], + [0x137, 1], + [0x138, -1], + [0x13e, 1], + [0x142, -1], + [0x143, 1], + [0x144, -1], + [0x147, 1], + [0x14b, -1], + [0x14c, 1], + [0x14d, -1], + [0x151, 1], + [0x153, -1], + [0x165, 1], + [0x167, -1], + [0x16a, 1], + [0x16b, -1], + [0x1cd, 1], + [0x1ce, -1], + [0x1cf, 1], + [0x1d0, -1], + [0x1d1, 1], + [0x1d2, -1], + [0x1d3, 1], + [0x1d4, -1], + [0x1d5, 1], + [0x1d6, -1], + [0x1d7, 1], + [0x1d8, -1], + [0x1d9, 1], + [0x1da, -1], + [0x1db, 1], + [0x1dc, -1], + [0x250, 1], + [0x251, -1], + [0x260, 1], + [0x261, -1], + [0x2c3, 1], + [0x2c4, -1], + [0x2c6, 1], + [0x2c7, -1], + [0x2c8, 1], + [0x2cb, -1], + [0x2cc, 1], + [0x2cd, -1], + [0x2cf, 1], + [0x2d0, -1], + [0x2d7, 1], + [0x2db, -1], + [0x2dc, 1], + [0x2dd, -1], + [0x2de, 1], + [0x2df, -1], + [0x2ff, 1], + [0x36f, 0], + [0x390, 1], + [0x3a1, -1], + [0x3a2, 1], + [0x3a9, -1], + [0x3b0, 1], + [0x3c1, -1], + [0x3c2, 1], + [0x3c9, -1], + [0x400, 1], + [0x401, -1], + [0x40f, 1], + [0x44f, -1], + [0x450, 1], + [0x451, -1], + [0x482, 1], + [0x487, 0], + [0x590, 1], + [0x5bd, 0], + [0x5be, 1], + [0x5bf, 0], + [0x5c0, 1], + [0x5c2, 0], + [0x5c3, 1], + [0x5c5, 0], + [0x5c6, 1], + [0x5c7, 0], + [0x60f, 1], + [0x61a, 0], + [0x64a, 1], + [0x65f, 0], + [0x66f, 1], + [0x670, 0], + [0x6d5, 1], + [0x6dc, 0], + [0x6de, 1], + [0x6e4, 0], + [0x6e6, 1], + [0x6e8, 0], + [0x6e9, 1], + [0x6ed, 0], + [0x710, 1], + [0x711, 0], + [0x72f, 1], + [0x74a, 0], + [0x7a5, 1], + [0x7b0, 0], + [0x7ea, 1], + [0x7f3, 0], + [0x7fc, 1], + [0x7fd, 0], + [0x815, 1], + [0x819, 0], + [0x81a, 1], + [0x823, 0], + [0x824, 1], + [0x827, 0], + [0x828, 1], + [0x82d, 0], + [0x858, 1], + [0x85b, 0], + [0x897, 1], + [0x89f, 0], + [0x8c9, 1], + [0x8e1, 0], + [0x8e2, 1], + [0x902, 0], + [0x939, 1], + [0x93a, 0], + [0x93b, 1], + [0x93c, 0], + [0x940, 1], + [0x948, 0], + [0x94c, 1], + [0x94d, 0], + [0x950, 1], + [0x957, 0], + [0x961, 1], + [0x963, 0], + [0x980, 1], + [0x981, 0], + [0x9bb, 1], + [0x9bc, 0], + [0x9c0, 1], + [0x9c4, 0], + [0x9cc, 1], + [0x9cd, 0], + [0x9e1, 1], + [0x9e3, 0], + [0x9fd, 1], + [0x9fe, 0], + [0xa00, 1], + [0xa02, 0], + [0xa3b, 1], + [0xa3c, 0], + [0xa40, 1], + [0xa42, 0], + [0xa46, 1], + [0xa48, 0], + [0xa4a, 1], + [0xa4d, 0], + [0xa50, 1], + [0xa51, 0], + [0xa6f, 1], + [0xa71, 0], + [0xa74, 1], + [0xa75, 0], + [0xa80, 1], + [0xa82, 0], + [0xabb, 1], + [0xabc, 0], + [0xac0, 1], + [0xac5, 0], + [0xac6, 1], + [0xac8, 0], + [0xacc, 1], + [0xacd, 0], + [0xae1, 1], + [0xae3, 0], + [0xaf9, 1], + [0xaff, 0], + [0xb00, 1], + [0xb01, 0], + [0xb3b, 1], + [0xb3c, 0], + [0xb3e, 1], + [0xb3f, 0], + [0xb40, 1], + [0xb44, 0], + [0xb4c, 1], + [0xb4d, 0], + [0xb54, 1], + [0xb56, 0], + [0xb61, 1], + [0xb63, 0], + [0xb81, 1], + [0xb82, 0], + [0xbbf, 1], + [0xbc0, 0], + [0xbcc, 1], + [0xbcd, 0], + [0xbff, 1], + [0xc00, 0], + [0xc03, 1], + [0xc04, 0], + [0xc3b, 1], + [0xc3c, 0], + [0xc3d, 1], + [0xc40, 0], + [0xc45, 1], + [0xc48, 0], + [0xc49, 1], + [0xc4d, 0], + [0xc54, 1], + [0xc56, 0], + [0xc61, 1], + [0xc63, 0], + [0xc80, 1], + [0xc81, 0], + [0xcbb, 1], + [0xcbc, 0], + [0xcbe, 1], + [0xcbf, 0], + [0xcc5, 1], + [0xcc6, 0], + [0xccb, 1], + [0xccd, 0], + [0xce1, 1], + [0xce3, 0], + [0xcff, 1], + [0xd01, 0], + [0xd3a, 1], + [0xd3c, 0], + [0xd40, 1], + [0xd44, 0], + [0xd4c, 1], + [0xd4d, 0], + [0xd61, 1], + [0xd63, 0], + [0xd80, 1], + [0xd81, 0], + [0xdc9, 1], + [0xdca, 0], + [0xdd1, 1], + [0xdd4, 0], + [0xdd5, 1], + [0xdd6, 0], + [0xe30, 1], + [0xe31, 0], + [0xe33, 1], + [0xe3a, 0], + [0xe46, 1], + [0xe4e, 0], + [0xeb0, 1], + [0xeb1, 0], + [0xeb3, 1], + [0xebc, 0], + [0xec7, 1], + [0xece, 0], + [0xf17, 1], + [0xf19, 0], + [0xf34, 1], + [0xf35, 0], + [0xf36, 1], + [0xf37, 0], + [0xf38, 1], + [0xf39, 0], + [0xf70, 1], + [0xf7e, 0], + [0xf7f, 1], + [0xf84, 0], + [0xf85, 1], + [0xf87, 0], + [0xf8c, 1], + [0xf97, 0], + [0xf98, 1], + [0xfbc, 0], + [0xfc5, 1], + [0xfc6, 0], + [0x102c, 1], + [0x1030, 0], + [0x1031, 1], + [0x1037, 0], + [0x1038, 1], + [0x103a, 0], + [0x103c, 1], + [0x103e, 0], + [0x1057, 1], + [0x1059, 0], + [0x105d, 1], + [0x1060, 0], + [0x1070, 1], + [0x1074, 0], + [0x1081, 1], + [0x1082, 0], + [0x1084, 1], + [0x1086, 0], + [0x108c, 1], + [0x108d, 0], + [0x109c, 1], + [0x109d, 0], + [0x10ff, 1], + [0x115f, 2], + [0x135c, 1], + [0x135f, 0], + [0x1711, 1], + [0x1714, 0], + [0x1731, 1], + [0x1733, 0], + [0x1751, 1], + [0x1753, 0], + [0x1771, 1], + [0x1773, 0], + [0x17b3, 1], + [0x17b5, 0], + [0x17b6, 1], + [0x17bd, 0], + [0x17c5, 1], + [0x17c6, 0], + [0x17c8, 1], + [0x17d3, 0], + [0x17dc, 1], + [0x17dd, 0], + [0x180a, 1], + [0x180d, 0], + [0x180e, 1], + [0x180f, 0], + [0x1884, 1], + [0x1886, 0], + [0x18a8, 1], + [0x18a9, 0], + [0x191f, 1], + [0x1922, 0], + [0x1926, 1], + [0x1928, 0], + [0x1931, 1], + [0x1932, 0], + [0x1938, 1], + [0x193b, 0], + [0x1a16, 1], + [0x1a18, 0], + [0x1a1a, 1], + [0x1a1b, 0], + [0x1a55, 1], + [0x1a56, 0], + [0x1a57, 1], + [0x1a5e, 0], + [0x1a5f, 1], + [0x1a60, 0], + [0x1a61, 1], + [0x1a62, 0], + [0x1a64, 1], + [0x1a6c, 0], + [0x1a72, 1], + [0x1a7c, 0], + [0x1a7e, 1], + [0x1a7f, 0], + [0x1aaf, 1], + [0x1abd, 0], + [0x1abe, 1], + [0x1ace, 0], + [0x1aff, 1], + [0x1b03, 0], + [0x1b33, 1], + [0x1b34, 0], + [0x1b35, 1], + [0x1b3a, 0], + [0x1b3b, 1], + [0x1b3c, 0], + [0x1b41, 1], + [0x1b42, 0], + [0x1b6a, 1], + [0x1b73, 0], + [0x1b7f, 1], + [0x1b81, 0], + [0x1ba1, 1], + [0x1ba5, 0], + [0x1ba7, 1], + [0x1ba9, 0], + [0x1baa, 1], + [0x1bad, 0], + [0x1be5, 1], + [0x1be6, 0], + [0x1be7, 1], + [0x1be9, 0], + [0x1bec, 1], + [0x1bed, 0], + [0x1bee, 1], + [0x1bf1, 0], + [0x1c2b, 1], + [0x1c33, 0], + [0x1c35, 1], + [0x1c37, 0], + [0x1ccf, 1], + [0x1cd2, 0], + [0x1cd3, 1], + [0x1ce0, 0], + [0x1ce1, 1], + [0x1ce8, 0], + [0x1cec, 1], + [0x1ced, 0], + [0x1cf3, 1], + [0x1cf4, 0], + [0x1cf7, 1], + [0x1cf9, 0], + [0x1dbf, 1], + [0x1dff, 0], + [0x200f, 1], + [0x2010, -1], + [0x2012, 1], + [0x2016, -1], + [0x2017, 1], + [0x2019, -1], + [0x201b, 1], + [0x201d, -1], + [0x201f, 1], + [0x2022, -1], + [0x2023, 1], + [0x2027, -1], + [0x202f, 1], + [0x2030, -1], + [0x2031, 1], + [0x2033, -1], + [0x2034, 1], + [0x2035, -1], + [0x203a, 1], + [0x203b, -1], + [0x203d, 1], + [0x203e, -1], + [0x2073, 1], + [0x2074, -1], + [0x207e, 1], + [0x207f, -1], + [0x2080, 1], + [0x2084, -1], + [0x20ab, 1], + [0x20ac, -1], + [0x20cf, 1], + [0x20dc, 0], + [0x20e0, 1], + [0x20e1, 0], + [0x20e4, 1], + [0x20f0, 0], + [0x2102, 1], + [0x2103, -1], + [0x2104, 1], + [0x2105, -1], + [0x2108, 1], + [0x2109, -1], + [0x2112, 1], + [0x2113, -1], + [0x2115, 1], + [0x2116, -1], + [0x2120, 1], + [0x2122, -1], + [0x2125, 1], + [0x2126, -1], + [0x212a, 1], + [0x212b, -1], + [0x2152, 1], + [0x2154, -1], + [0x215a, 1], + [0x215e, -1], + [0x215f, 1], + [0x216b, -1], + [0x216f, 1], + [0x2179, -1], + [0x2188, 1], + [0x2189, -1], + [0x218f, 1], + [0x2199, -1], + [0x21b7, 1], + [0x21b9, -1], + [0x21d1, 1], + [0x21d2, -1], + [0x21d3, 1], + [0x21d4, -1], + [0x21e6, 1], + [0x21e7, -1], + [0x21ff, 1], + [0x2200, -1], + [0x2201, 1], + [0x2203, -1], + [0x2206, 1], + [0x2208, -1], + [0x220a, 1], + [0x220b, -1], + [0x220e, 1], + [0x220f, -1], + [0x2210, 1], + [0x2211, -1], + [0x2214, 1], + [0x2215, -1], + [0x2219, 1], + [0x221a, -1], + [0x221c, 1], + [0x2220, -1], + [0x2222, 1], + [0x2223, -1], + [0x2224, 1], + [0x2225, -1], + [0x2226, 1], + [0x222c, -1], + [0x222d, 1], + [0x222e, -1], + [0x2233, 1], + [0x2237, -1], + [0x223b, 1], + [0x223d, -1], + [0x2247, 1], + [0x2248, -1], + [0x224b, 1], + [0x224c, -1], + [0x2251, 1], + [0x2252, -1], + [0x225f, 1], + [0x2261, -1], + [0x2263, 1], + [0x2267, -1], + [0x2269, 1], + [0x226b, -1], + [0x226d, 1], + [0x226f, -1], + [0x2281, 1], + [0x2283, -1], + [0x2285, 1], + [0x2287, -1], + [0x2294, 1], + [0x2295, -1], + [0x2298, 1], + [0x2299, -1], + [0x22a4, 1], + [0x22a5, -1], + [0x22be, 1], + [0x22bf, -1], + [0x2311, 1], + [0x2312, -1], + [0x2319, 1], + [0x231b, 2], + [0x2328, 1], + [0x232a, 2], + [0x23e8, 1], + [0x23ec, 2], + [0x23ef, 1], + [0x23f0, 2], + [0x23f2, 1], + [0x23f3, 2], + [0x245f, 1], + [0x24e9, -1], + [0x24ea, 1], + [0x254b, -1], + [0x254f, 1], + [0x2573, -1], + [0x257f, 1], + [0x258f, -1], + [0x2591, 1], + [0x2595, -1], + [0x259f, 1], + [0x25a1, -1], + [0x25a2, 1], + [0x25a9, -1], + [0x25b1, 1], + [0x25b3, -1], + [0x25b5, 1], + [0x25b7, -1], + [0x25bb, 1], + [0x25bd, -1], + [0x25bf, 1], + [0x25c1, -1], + [0x25c5, 1], + [0x25c8, -1], + [0x25ca, 1], + [0x25cb, -1], + [0x25cd, 1], + [0x25d1, -1], + [0x25e1, 1], + [0x25e5, -1], + [0x25ee, 1], + [0x25ef, -1], + [0x25fc, 1], + [0x25fe, 2], + [0x2604, 1], + [0x2606, -1], + [0x2608, 1], + [0x2609, -1], + [0x260d, 1], + [0x260f, -1], + [0x2613, 1], + [0x2615, 2], + [0x261b, 1], + [0x261c, -1], + [0x261d, 1], + [0x261e, -1], + [0x263f, 1], + [0x2640, -1], + [0x2641, 1], + [0x2642, -1], + [0x2647, 1], + [0x2653, 2], + [0x265f, 1], + [0x2661, -1], + [0x2662, 1], + [0x2665, -1], + [0x2666, 1], + [0x266a, -1], + [0x266b, 1], + [0x266d, -1], + [0x266e, 1], + [0x266f, -1], + [0x267e, 1], + [0x267f, 2], + [0x2692, 1], + [0x2693, 2], + [0x269d, 1], + [0x269f, -1], + [0x26a0, 1], + [0x26a1, 2], + [0x26a9, 1], + [0x26ab, 2], + [0x26bc, 1], + [0x26be, 2], + [0x26bf, -1], + [0x26c3, 1], + [0x26c5, 2], + [0x26cd, -1], + [0x26ce, 2], + [0x26d3, -1], + [0x26d4, 2], + [0x26e1, -1], + [0x26e2, 1], + [0x26e3, -1], + [0x26e7, 1], + [0x26e9, -1], + [0x26ea, 2], + [0x26f1, -1], + [0x26f3, 2], + [0x26f4, -1], + [0x26f5, 2], + [0x26f9, -1], + [0x26fa, 2], + [0x26fc, -1], + [0x26fd, 2], + [0x26ff, -1], + [0x2704, 1], + [0x2705, 2], + [0x2709, 1], + [0x270b, 2], + [0x2727, 1], + [0x2728, 2], + [0x273c, 1], + [0x273d, -1], + [0x274b, 1], + [0x274c, 2], + [0x274d, 1], + [0x274e, 2], + [0x2752, 1], + [0x2755, 2], + [0x2756, 1], + [0x2757, 2], + [0x2775, 1], + [0x277f, -1], + [0x2794, 1], + [0x2797, 2], + [0x27af, 1], + [0x27b0, 2], + [0x27be, 1], + [0x27bf, 2], + [0x2b1a, 1], + [0x2b1c, 2], + [0x2b4f, 1], + [0x2b50, 2], + [0x2b54, 1], + [0x2b55, 2], + [0x2b59, -1], + [0x2cee, 1], + [0x2cf1, 0], + [0x2d7e, 1], + [0x2d7f, 0], + [0x2ddf, 1], + [0x2dff, 0], + [0x2e7f, 1], + [0x2e99, 2], + [0x2e9a, 1], + [0x2ef3, 2], + [0x2eff, 1], + [0x2fd5, 2], + [0x2fef, 1], + [0x3029, 2], + [0x302d, 0], + [0x303e, 2], + [0x3040, 1], + [0x3096, 2], + [0x3098, 1], + [0x309a, 0], + [0x30ff, 2], + [0x3104, 1], + [0x312f, 2], + [0x3130, 1], + [0x318e, 2], + [0x318f, 1], + [0x31e3, 2], + [0x31ee, 1], + [0x321e, 2], + [0x321f, 1], + [0x3247, 2], + [0x324f, -1], + [0x4dbf, 2], + [0x4dff, 1], + [0xa48c, 2], + [0xa48f, 1], + [0xa4c6, 2], + [0xa66e, 1], + [0xa66f, 0], + [0xa673, 1], + [0xa67d, 0], + [0xa69d, 1], + [0xa69f, 0], + [0xa6ef, 1], + [0xa6f1, 0], + [0xa801, 1], + [0xa802, 0], + [0xa805, 1], + [0xa806, 0], + [0xa80a, 1], + [0xa80b, 0], + [0xa824, 1], + [0xa826, 0], + [0xa82b, 1], + [0xa82c, 0], + [0xa8c3, 1], + [0xa8c5, 0], + [0xa8df, 1], + [0xa8f1, 0], + [0xa8fe, 1], + [0xa8ff, 0], + [0xa925, 1], + [0xa92d, 0], + [0xa946, 1], + [0xa951, 0], + [0xa95f, 1], + [0xa97c, 2], + [0xa97f, 1], + [0xa982, 0], + [0xa9b2, 1], + [0xa9b3, 0], + [0xa9b5, 1], + [0xa9b9, 0], + [0xa9bb, 1], + [0xa9bd, 0], + [0xa9e4, 1], + [0xa9e5, 0], + [0xaa28, 1], + [0xaa2e, 0], + [0xaa30, 1], + [0xaa32, 0], + [0xaa34, 1], + [0xaa36, 0], + [0xaa42, 1], + [0xaa43, 0], + [0xaa4b, 1], + [0xaa4c, 0], + [0xaa7b, 1], + [0xaa7c, 0], + [0xaaaf, 1], + [0xaab0, 0], + [0xaab1, 1], + [0xaab4, 0], + [0xaab6, 1], + [0xaab8, 0], + [0xaabd, 1], + [0xaabf, 0], + [0xaac0, 1], + [0xaac1, 0], + [0xaaeb, 1], + [0xaaed, 0], + [0xaaf5, 1], + [0xaaf6, 0], + [0xabe4, 1], + [0xabe5, 0], + [0xabe7, 1], + [0xabe8, 0], + [0xabec, 1], + [0xabed, 0], + [0xabff, 1], + [0xd7a3, 2], + [0xdfff, 1], + [0xf8ff, -1], + [0xfaff, 2], + [0xfb1d, 1], + [0xfb1e, 0], + [0xfdff, 1], + [0xfe0f, 0], + [0xfe19, 2], + [0xfe1f, 1], + [0xfe2f, 0], + [0xfe52, 2], + [0xfe53, 1], + [0xfe66, 2], + [0xfe67, 1], + [0xfe6b, 2], + [0xff00, 1], + [0xff60, 2], + [0xffdf, 1], + [0xffe6, 2], + [0xfffc, 1], + [0xfffd, -1], + [0x101fc, 1], + [0x101fd, 0], + [0x102df, 1], + [0x102e0, 0], + [0x10375, 1], + [0x1037a, 0], + [0x10a00, 1], + [0x10a03, 0], + [0x10a04, 1], + [0x10a06, 0], + [0x10a0b, 1], + [0x10a0f, 0], + [0x10a37, 1], + [0x10a3a, 0], + [0x10a3e, 1], + [0x10a3f, 0], + [0x10ae4, 1], + [0x10ae6, 0], + [0x10d23, 1], + [0x10d27, 0], + [0x10eaa, 1], + [0x10eac, 0], + [0x10efc, 1], + [0x10eff, 0], + [0x10f45, 1], + [0x10f50, 0], + [0x10f81, 1], + [0x10f85, 0], + [0x11000, 1], + [0x11001, 0], + [0x11037, 1], + [0x11046, 0], + [0x1106f, 1], + [0x11070, 0], + [0x11072, 1], + [0x11074, 0], + [0x1107e, 1], + [0x11081, 0], + [0x110b2, 1], + [0x110b6, 0], + [0x110b8, 1], + [0x110ba, 0], + [0x110c1, 1], + [0x110c2, 0], + [0x110ff, 1], + [0x11102, 0], + [0x11126, 1], + [0x1112b, 0], + [0x1112c, 1], + [0x11134, 0], + [0x11172, 1], + [0x11173, 0], + [0x1117f, 1], + [0x11181, 0], + [0x111b5, 1], + [0x111be, 0], + [0x111c8, 1], + [0x111cc, 0], + [0x111ce, 1], + [0x111cf, 0], + [0x1122e, 1], + [0x11231, 0], + [0x11233, 1], + [0x11234, 0], + [0x11235, 1], + [0x11237, 0], + [0x1123d, 1], + [0x1123e, 0], + [0x11240, 1], + [0x11241, 0], + [0x112de, 1], + [0x112df, 0], + [0x112e2, 1], + [0x112ea, 0], + [0x112ff, 1], + [0x11301, 0], + [0x1133a, 1], + [0x1133c, 0], + [0x1133f, 1], + [0x11340, 0], + [0x11365, 1], + [0x1136c, 0], + [0x1136f, 1], + [0x11374, 0], + [0x11437, 1], + [0x1143f, 0], + [0x11441, 1], + [0x11444, 0], + [0x11445, 1], + [0x11446, 0], + [0x1145d, 1], + [0x1145e, 0], + [0x114b2, 1], + [0x114b8, 0], + [0x114b9, 1], + [0x114ba, 0], + [0x114be, 1], + [0x114c0, 0], + [0x114c1, 1], + [0x114c3, 0], + [0x115b1, 1], + [0x115b5, 0], + [0x115bb, 1], + [0x115bd, 0], + [0x115be, 1], + [0x115c0, 0], + [0x115db, 1], + [0x115dd, 0], + [0x11632, 1], + [0x1163a, 0], + [0x1163c, 1], + [0x1163d, 0], + [0x1163e, 1], + [0x11640, 0], + [0x116aa, 1], + [0x116ab, 0], + [0x116ac, 1], + [0x116ad, 0], + [0x116af, 1], + [0x116b5, 0], + [0x116b6, 1], + [0x116b7, 0], + [0x1171c, 1], + [0x1171f, 0], + [0x11721, 1], + [0x11725, 0], + [0x11726, 1], + [0x1172b, 0], + [0x1182e, 1], + [0x11837, 0], + [0x11838, 1], + [0x1183a, 0], + [0x1193a, 1], + [0x1193c, 0], + [0x1193d, 1], + [0x1193e, 0], + [0x11942, 1], + [0x11943, 0], + [0x119d3, 1], + [0x119d7, 0], + [0x119d9, 1], + [0x119db, 0], + [0x119df, 1], + [0x119e0, 0], + [0x11a00, 1], + [0x11a0a, 0], + [0x11a32, 1], + [0x11a38, 0], + [0x11a3a, 1], + [0x11a3e, 0], + [0x11a46, 1], + [0x11a47, 0], + [0x11a50, 1], + [0x11a56, 0], + [0x11a58, 1], + [0x11a5b, 0], + [0x11a89, 1], + [0x11a96, 0], + [0x11a97, 1], + [0x11a99, 0], + [0x11c2f, 1], + [0x11c36, 0], + [0x11c37, 1], + [0x11c3d, 0], + [0x11c3e, 1], + [0x11c3f, 0], + [0x11c91, 1], + [0x11ca7, 0], + [0x11ca9, 1], + [0x11cb0, 0], + [0x11cb1, 1], + [0x11cb3, 0], + [0x11cb4, 1], + [0x11cb6, 0], + [0x11d30, 1], + [0x11d36, 0], + [0x11d39, 1], + [0x11d3a, 0], + [0x11d3b, 1], + [0x11d3d, 0], + [0x11d3e, 1], + [0x11d45, 0], + [0x11d46, 1], + [0x11d47, 0], + [0x11d8f, 1], + [0x11d91, 0], + [0x11d94, 1], + [0x11d95, 0], + [0x11d96, 1], + [0x11d97, 0], + [0x11ef2, 1], + [0x11ef4, 0], + [0x11eff, 1], + [0x11f01, 0], + [0x11f35, 1], + [0x11f3a, 0], + [0x11f3f, 1], + [0x11f40, 0], + [0x11f41, 1], + [0x11f42, 0], + [0x1343f, 1], + [0x13440, 0], + [0x13446, 1], + [0x13455, 0], + [0x16aef, 1], + [0x16af4, 0], + [0x16b2f, 1], + [0x16b36, 0], + [0x16f4e, 1], + [0x16f4f, 0], + [0x16f8e, 1], + [0x16f92, 0], + [0x16fdf, 1], + [0x16fe3, 2], + [0x16fe4, 0], + [0x16fef, 1], + [0x16ff1, 2], + [0x16fff, 1], + [0x187f7, 2], + [0x187ff, 1], + [0x18cd5, 2], + [0x18cff, 1], + [0x18d08, 2], + [0x1afef, 1], + [0x1aff3, 2], + [0x1aff4, 1], + [0x1affb, 2], + [0x1affc, 1], + [0x1affe, 2], + [0x1afff, 1], + [0x1b122, 2], + [0x1b131, 1], + [0x1b132, 2], + [0x1b14f, 1], + [0x1b152, 2], + [0x1b154, 1], + [0x1b155, 2], + [0x1b163, 1], + [0x1b167, 2], + [0x1b16f, 1], + [0x1b2fb, 2], + [0x1bc9c, 1], + [0x1bc9e, 0], + [0x1ceff, 1], + [0x1cf2d, 0], + [0x1cf2f, 1], + [0x1cf46, 0], + [0x1d166, 1], + [0x1d169, 0], + [0x1d17a, 1], + [0x1d182, 0], + [0x1d184, 1], + [0x1d18b, 0], + [0x1d1a9, 1], + [0x1d1ad, 0], + [0x1d241, 1], + [0x1d244, 0], + [0x1d9ff, 1], + [0x1da36, 0], + [0x1da3a, 1], + [0x1da6c, 0], + [0x1da74, 1], + [0x1da75, 0], + [0x1da83, 1], + [0x1da84, 0], + [0x1da9a, 1], + [0x1da9f, 0], + [0x1daa0, 1], + [0x1daaf, 0], + [0x1dfff, 1], + [0x1e006, 0], + [0x1e007, 1], + [0x1e018, 0], + [0x1e01a, 1], + [0x1e021, 0], + [0x1e022, 1], + [0x1e024, 0], + [0x1e025, 1], + [0x1e02a, 0], + [0x1e08e, 1], + [0x1e08f, 0], + [0x1e12f, 1], + [0x1e136, 0], + [0x1e2ad, 1], + [0x1e2ae, 0], + [0x1e2eb, 1], + [0x1e2ef, 0], + [0x1e4eb, 1], + [0x1e4ef, 0], + [0x1e8cf, 1], + [0x1e8d6, 0], + [0x1e943, 1], + [0x1e94a, 0], + [0x1f003, 1], + [0x1f004, 2], + [0x1f0ce, 1], + [0x1f0cf, 2], + [0x1f0ff, 1], + [0x1f10a, -1], + [0x1f10f, 1], + [0x1f12d, -1], + [0x1f12f, 1], + [0x1f169, -1], + [0x1f16f, 1], + [0x1f18d, -1], + [0x1f18e, 2], + [0x1f190, -1], + [0x1f19a, 2], + [0x1f1ac, -1], + [0x1f1ff, 1], + [0x1f202, 2], + [0x1f20f, 1], + [0x1f23b, 2], + [0x1f23f, 1], + [0x1f248, 2], + [0x1f24f, 1], + [0x1f251, 2], + [0x1f25f, 1], + [0x1f265, 2], + [0x1f2ff, 1], + [0x1f320, 2], + [0x1f32c, 1], + [0x1f335, 2], + [0x1f336, 1], + [0x1f37c, 2], + [0x1f37d, 1], + [0x1f393, 2], + [0x1f39f, 1], + [0x1f3ca, 2], + [0x1f3ce, 1], + [0x1f3d3, 2], + [0x1f3df, 1], + [0x1f3f0, 2], + [0x1f3f3, 1], + [0x1f3f4, 2], + [0x1f3f7, 1], + [0x1f43e, 2], + [0x1f43f, 1], + [0x1f440, 2], + [0x1f441, 1], + [0x1f4fc, 2], + [0x1f4fe, 1], + [0x1f53d, 2], + [0x1f54a, 1], + [0x1f54e, 2], + [0x1f54f, 1], + [0x1f567, 2], + [0x1f579, 1], + [0x1f57a, 2], + [0x1f594, 1], + [0x1f596, 2], + [0x1f5a3, 1], + [0x1f5a4, 2], + [0x1f5fa, 1], + [0x1f64f, 2], + [0x1f67f, 1], + [0x1f6c5, 2], + [0x1f6cb, 1], + [0x1f6cc, 2], + [0x1f6cf, 1], + [0x1f6d2, 2], + [0x1f6d4, 1], + [0x1f6d7, 2], + [0x1f6db, 1], + [0x1f6df, 2], + [0x1f6ea, 1], + [0x1f6ec, 2], + [0x1f6f3, 1], + [0x1f6fc, 2], + [0x1f7df, 1], + [0x1f7eb, 2], + [0x1f7ef, 1], + [0x1f7f0, 2], + [0x1f90b, 1], + [0x1f93a, 2], + [0x1f93b, 1], + [0x1f945, 2], + [0x1f946, 1], + [0x1f9ff, 2], + [0x1fa6f, 1], + [0x1fa7c, 2], + [0x1fa7f, 1], + [0x1fa88, 2], + [0x1fa8f, 1], + [0x1fabd, 2], + [0x1fabe, 1], + [0x1fac5, 2], + [0x1facd, 1], + [0x1fadb, 2], + [0x1fadf, 1], + [0x1fae8, 2], + [0x1faef, 1], + [0x1faf8, 2], + [0x1ffff, 1], + [0x2fffd, 2], + [0x2ffff, 1], + [0x3fffd, 2], + [0xe00ff, 1], + [0xe01ef, 0], + [0xeffff, 1], + [0xffffd, -1], + [0xfffff, 1], + [0x10fffd, -1], + [0x7fffffff, 1] + ].transpose.map(&:freeze) end diff --git a/lib/reline/version.rb b/lib/reline/version.rb index ef7d617a45..b75d874adb 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.5.6' + VERSION = '0.5.10' end diff --git a/lib/resolv.gemspec b/lib/resolv.gemspec index 6b83e303d7..bfa2f9ff31 100644 --- a/lib/resolv.gemspec +++ b/lib/resolv.gemspec @@ -16,6 +16,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/resolv" spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") spec.licenses = ["Ruby", "BSD-2-Clause"] + spec.extensions << "ext/win32/resolv/extconf.rb" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage diff --git a/lib/resolv.rb b/lib/resolv.rb index e36dbce259..0574ce622b 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -513,35 +513,40 @@ class Resolv def fetch_resource(name, typeclass) lazy_initialize - begin - requester = make_udp_requester + truncated = {} + requesters = {} + udp_requester = begin + make_udp_requester rescue Errno::EACCES # fall back to TCP end senders = {} + begin - @config.resolv(name) {|candidate, tout, nameserver, port| - requester ||= make_tcp_requester(nameserver, port) + @config.resolv(name) do |candidate, tout, nameserver, port| msg = Message.new msg.rd = 1 msg.add_question(candidate, typeclass) - unless sender = senders[[candidate, nameserver, port]] + + requester = requesters.fetch([nameserver, port]) do + if !truncated[candidate] && udp_requester + udp_requester + else + requesters[[nameserver, port]] = make_tcp_requester(nameserver, port) + end + end + + unless sender = senders[[candidate, requester, nameserver, port]] sender = requester.sender(msg, candidate, nameserver, port) next if !sender - senders[[candidate, nameserver, port]] = sender + senders[[candidate, requester, nameserver, port]] = sender end reply, reply_name = requester.request(sender, tout) case reply.rcode when RCode::NoError if reply.tc == 1 and not Requester::TCP === requester - requester.close # Retry via TCP: - requester = make_tcp_requester(nameserver, port) - senders = {} - # This will use TCP for all remaining candidates (assuming the - # current candidate does not already respond successfully via - # TCP). This makes sense because we already know the full - # response will not fit in an untruncated UDP packet. + truncated[candidate] = true redo else yield(reply, reply_name) @@ -552,9 +557,10 @@ class Resolv else raise Config::OtherResolvError.new(reply_name.to_s) end - } + end ensure - requester&.close + udp_requester&.close + requesters.each_value { |requester| requester&.close } end end @@ -569,6 +575,11 @@ class Resolv def make_tcp_requester(host, port) # :nodoc: return Requester::TCP.new(host, port) + rescue Errno::ECONNREFUSED + # Treat a refused TCP connection attempt to a nameserver like a timeout, + # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a + # hint to try the next nameserver: + raise ResolvTimeout end def extract_resources(msg, name, typeclass) # :nodoc: @@ -1800,7 +1811,6 @@ class Resolv end end - ## # Base class for SvcParam. [RFC9460] @@ -2499,7 +2509,6 @@ class Resolv attr_reader :altitude - def encode_rdata(msg) # :nodoc: msg.put_bytes(@version) msg.put_bytes(@ssize.scalar) @@ -3439,4 +3448,3 @@ class Resolv AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ end - diff --git a/lib/ruby_vm/rjit/assembler.rb b/lib/ruby_vm/rjit/assembler.rb index 645072d11b..42995e6c8c 100644 --- a/lib/ruby_vm/rjit/assembler.rb +++ b/lib/ruby_vm/rjit/assembler.rb @@ -152,6 +152,16 @@ module RubyVM::RJIT mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], disp: imm8(src_disp), ) + # AND r64, r/m64 (Mod 10: [reg]+disp32) + in [R64 => dst_reg, QwordPtr[R64 => src_reg, IMM32 => src_disp]] + # REX.W + 23 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x23, + mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg], + disp: imm32(src_disp), + ) end end @@ -736,6 +746,16 @@ module RubyVM::RJIT mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], disp: imm8(src_disp), ) + # OR r64, r/m64 (Mod 10: [reg]+disp32) + in [R64 => dst_reg, QwordPtr[R64 => src_reg, IMM32 => src_disp]] + # REX.W + 0B /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x0b, + mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg], + disp: imm32(src_disp), + ) end end diff --git a/lib/ruby_vm/rjit/insn_compiler.rb b/lib/ruby_vm/rjit/insn_compiler.rb index f9450241c9..a33ba9f468 100644 --- a/lib/ruby_vm/rjit/insn_compiler.rb +++ b/lib/ruby_vm/rjit/insn_compiler.rb @@ -63,7 +63,6 @@ module RubyVM::RJIT when :toregexp then toregexp(jit, ctx, asm) when :intern then intern(jit, ctx, asm) when :newarray then newarray(jit, ctx, asm) - # newarraykwsplat when :duparray then duparray(jit, ctx, asm) # duphash when :expandarray then expandarray(jit, ctx, asm) @@ -91,6 +90,8 @@ module RubyVM::RJIT when :opt_send_without_block then opt_send_without_block(jit, ctx, asm) when :objtostring then objtostring(jit, ctx, asm) when :opt_str_freeze then opt_str_freeze(jit, ctx, asm) + when :opt_ary_freeze then opt_ary_freeze(jit, ctx, asm) + when :opt_hash_freeze then opt_hash_freeze(jit, ctx, asm) when :opt_nil_p then opt_nil_p(jit, ctx, asm) # opt_str_uminus when :opt_newarray_send then opt_newarray_send(jit, ctx, asm) @@ -504,7 +505,7 @@ module RubyVM::RJIT shape = C.rb_shape_get_shape_by_id(shape_id) current_capacity = shape.capacity - dest_shape = C.rb_shape_get_next(shape, comptime_receiver, ivar_name) + dest_shape = C.rb_shape_get_next_no_warnings(shape, comptime_receiver, ivar_name) new_shape_id = C.rb_shape_id(dest_shape) if new_shape_id == C::OBJ_TOO_COMPLEX_SHAPE_ID @@ -944,8 +945,6 @@ module RubyVM::RJIT KeepCompiling end - # newarraykwsplat - # @param jit [RubyVM::RJIT::JITState] # @param ctx [RubyVM::RJIT::Context] # @param asm [RubyVM::RJIT::Assembler] @@ -1435,6 +1434,10 @@ module RubyVM::RJIT mid = C.vm_ci_mid(cd.ci) calling = build_calling(ci: cd.ci, block_handler: blockiseq) + if calling.flags & C::VM_CALL_FORWARDING != 0 + return CantCompile + end + # vm_sendish cme, comptime_recv_klass = jit_search_method(jit, ctx, asm, mid, calling) if cme == CantCompile @@ -1492,6 +1495,42 @@ module RubyVM::RJIT # @param jit [RubyVM::RJIT::JITState] # @param ctx [RubyVM::RJIT::Context] # @param asm [RubyVM::RJIT::Assembler] + def opt_ary_freeze(jit, ctx, asm) + unless Invariants.assume_bop_not_redefined(jit, C::ARRAY_REDEFINED_OP_FLAG, C::BOP_FREEZE) + return CantCompile; + end + + ary = jit.operand(0, ruby: true) + + # Push the return value onto the stack + stack_ret = ctx.stack_push(Type::CArray) + asm.mov(:rax, to_value(ary)) + asm.mov(stack_ret, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::RJIT::JITState] + # @param ctx [RubyVM::RJIT::Context] + # @param asm [RubyVM::RJIT::Assembler] + def opt_hash_freeze(jit, ctx, asm) + unless Invariants.assume_bop_not_redefined(jit, C::HASH_REDEFINED_OP_FLAG, C::BOP_FREEZE) + return CantCompile; + end + + hash = jit.operand(0, ruby: true) + + # Push the return value onto the stack + stack_ret = ctx.stack_push(Type::CHash) + asm.mov(:rax, to_value(hash)) + asm.mov(stack_ret, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::RJIT::JITState] + # @param ctx [RubyVM::RJIT::Context] + # @param asm [RubyVM::RJIT::Assembler] def opt_str_freeze(jit, ctx, asm) unless Invariants.assume_bop_not_redefined(jit, C::STRING_REDEFINED_OP_FLAG, C::BOP_FREEZE) return CantCompile; @@ -4622,6 +4661,11 @@ module RubyVM::RJIT end end + # Don't compile forwardable iseqs + if iseq.body.param.flags.forwardable + return CantCompile + end + # We will not have CantCompile from here. if block_arg diff --git a/lib/rubygems.rb b/lib/rubygems.rb index ad7ab10756..43c0d8f13c 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -18,6 +18,7 @@ require_relative "rubygems/compatibility" require_relative "rubygems/defaults" require_relative "rubygems/deprecate" require_relative "rubygems/errors" +require_relative "rubygems/target_rbconfig" ## # RubyGems is the Ruby standard for publishing and managing third party @@ -179,6 +180,8 @@ module Gem @discover_gems_on_require = true + @target_rbconfig = nil + ## # Try to activate a gem containing +path+. Returns true if # activation succeeded or wasn't needed because it was already @@ -397,6 +400,23 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## + # The RbConfig object for the deployment target platform. + # + # This is usually the same as the running platform, but may be + # different if you are cross-compiling. + + def self.target_rbconfig + @target_rbconfig || Gem::TargetRbConfig.for_running_ruby + end + + def self.set_target_rbconfig(rbconfig_path) + @target_rbconfig = Gem::TargetRbConfig.from_path(rbconfig_path) + Gem::Platform.local(refresh: true) + Gem.platforms << Gem::Platform.local unless Gem.platforms.include? Gem::Platform.local + @target_rbconfig + end + + ## # Quietly ensure the Gem directory +dir+ contains all the proper # subdirectories. If we can't create a directory due to a permission # problem, then we will silently continue. @@ -450,7 +470,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # distinction as extensions cannot be shared between the two. def self.extension_api_version # :nodoc: - if RbConfig::CONFIG["ENABLE_SHARED"] == "no" + if target_rbconfig["ENABLE_SHARED"] == "no" "#{ruby_api_version}-static" else ruby_api_version @@ -753,17 +773,14 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Safely read a file in binary mode on all platforms. def self.read_binary(path) - open_file(path, "rb+", &:read) - rescue Errno::EACCES, Errno::EROFS - open_file(path, "rb", &:read) + File.binread(path) end ## # Safely write a file in binary mode on all platforms. + def self.write_binary(path, data) - open_file(path, "wb") do |io| - io.write data - end + File.binwrite(path, data) rescue Errno::ENOSPC # If we ran out of space but the file exists, it's *guaranteed* to be corrupted. File.delete(path) if File.exist?(path) @@ -771,26 +788,38 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## - # Open a file with given flags, and on Windows protect access with flock + # Open a file with given flags def self.open_file(path, flags, &block) - File.open(path, flags) do |io| - if !java_platform? && win_platform? - begin - io.flock(File::LOCK_EX) - rescue Errno::ENOSYS, Errno::ENOTSUP - end + File.open(path, flags, &block) + end + + ## + # Open a file with given flags, and protect access with a file lock + + def self.open_file_with_lock(path, &block) + file_lock = "#{path}.lock" + open_file_with_flock(file_lock, &block) + ensure + FileUtils.rm_f file_lock + end + + ## + # Open a file with given flags, and protect access with flock + + def self.open_file_with_flock(path, &block) + mode = IO::RDONLY | IO::APPEND | IO::CREAT | IO::BINARY + mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE) + + File.open(path, mode) do |io| + begin + io.flock(File::LOCK_EX) + rescue Errno::ENOSYS, Errno::ENOTSUP + rescue Errno::ENOLCK # NFS + raise unless Thread.main == Thread.current end yield io end - rescue Errno::ENOLCK # NFS - if Thread.main != Thread.current - raise - else - File.open(path, flags) do |io| - yield io - end - end end ## @@ -810,7 +839,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} # Returns a String containing the API compatibility version of Ruby def self.ruby_api_version - @ruby_api_version ||= RbConfig::CONFIG["ruby_version"].dup + @ruby_api_version ||= target_rbconfig["ruby_version"].dup end def self.env_requirement(gem_name) @@ -1013,6 +1042,13 @@ An Array (#{env.inspect}) was passed in from #{caller[3]} end ## + # Is this platform FreeBSD + + def self.freebsd_platform? + RbConfig::CONFIG["host_os"].to_s.include?("bsd") + end + + ## # Load +plugins+ as Ruby files def self.load_plugin_files(plugins) # :nodoc: diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index 0380fceece..204231e95e 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -99,6 +99,20 @@ class Gem::BasicSpecification end ## + # Regular gems take precedence over default gems + + def default_gem_priority + default_gem? ? 1 : -1 + end + + ## + # Gems higher up in +gem_path+ take precedence + + def base_dir_priority(gem_path) + gem_path.index(base_dir) || gem_path.size + end + + ## # Returns full path to the directory where gem's extensions are installed. def extension_dir @@ -115,7 +129,6 @@ class Gem::BasicSpecification end def find_full_gem_path # :nodoc: - # TODO: also, shouldn't it default to full_name if it hasn't been written? File.expand_path File.join(gems_dir, full_name) end @@ -123,10 +136,10 @@ class Gem::BasicSpecification ## # The full path to the gem (install path + full name). + # + # TODO: This is duplicated with #gem_dir. Eventually either of them should be deprecated. def full_gem_path - # TODO: This is a heavily used method by gems, so we'll need - # to aleast just alias it to #gem_dir rather than remove it. @full_gem_path ||= find_full_gem_path end @@ -144,6 +157,19 @@ class Gem::BasicSpecification end ## + # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`). + # Information about where the gem is installed is also included if not + # installed in the default GEM_HOME. + + def full_name_with_location + if base_dir != Gem.dir + "#{full_name} in #{base_dir}" + else + full_name + end + end + + ## # Full paths in the gem to add to <code>$LOAD_PATH</code> when this gem is # activated. @@ -189,9 +215,11 @@ class Gem::BasicSpecification ## # Returns the full path to this spec's gem directory. # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0 + # + # TODO: This is duplicated with #full_gem_path. Eventually either of them should be deprecated. def gem_dir - @gem_dir ||= File.expand_path File.join(gems_dir, full_name) + @gem_dir ||= find_full_gem_path end ## diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index dd2fd77418..4ebbad1c0c 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -21,7 +21,7 @@ module Gem::BundlerVersionFinder end def self.bundle_update_bundler_version - return unless File.basename($0) == "bundle" + return unless ["bundle", "bundler"].include? File.basename($0) return unless "update".start_with?(ARGV.first || " ") bundler_version = nil update_index = nil diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb index d588804290..7985b0fda6 100644 --- a/lib/rubygems/commands/exec_command.rb +++ b/lib/rubygems/commands/exec_command.rb @@ -57,8 +57,6 @@ to the same gem path as user-installed gems. end def execute - gem_paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => Gem.paths.path.join(File::PATH_SEPARATOR), "GEM_SPEC_CACHE" => Gem.paths.spec_cache_dir }.compact - check_executable print_command @@ -74,9 +72,6 @@ to the same gem path as user-installed gems. end load! - ensure - ENV.update(gem_paths) if gem_paths - Gem.clear_paths end private @@ -143,7 +138,7 @@ to the same gem path as user-installed gems. end def set_gem_exec_install_paths - home = File.join(Gem.dir, "gem_exec") + home = Gem.dir ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR) ENV["GEM_HOME"] = home diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index f7f5b62306..c524cf7922 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -63,6 +63,17 @@ then repackaging it. def execute check_version + + exit_code = fetch_gems + + terminate_interaction exit_code + end + + private + + def fetch_gems + exit_code = 0 + version = options[:version] platform = Gem.platforms.last @@ -86,10 +97,13 @@ then repackaging it. if spec.nil? show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain] + exit_code |= 2 next end source.download spec say "Downloaded #{spec.full_name}" end + + exit_code end end diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 2091634a29..2888b6c55a 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -224,10 +224,6 @@ You can use `i` command instead of `install`. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 - rescue Gem::GemNotFoundException => e - show_lookup_failure e.name, e.version, e.errors, suppress_suggestions - - exit_code |= 2 rescue Gem::UnsatisfiableDependencyError => e show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, "'#{gem_name}' (#{gem_version})" diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 456d897df2..999c9fef0f 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -57,7 +57,7 @@ class Gem::Commands::PristineCommand < Gem::Command end add_option("-i", "--install-dir DIR", - "Gem repository to get binstubs and plugins installed") do |value, options| + "Gem repository to get gems restored") do |value, options| options[:install_dir] = File.expand_path(value) end @@ -103,21 +103,25 @@ extensions will be restored. end def execute + install_dir = options[:install_dir] + + specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record + specs = if options[:all] - Gem::Specification.map + specification_record.map # `--extensions` must be explicitly given to pristine only gems # with extensions. elsif options[:extensions_set] && options[:extensions] && options[:args].empty? - Gem::Specification.select do |spec| + specification_record.select do |spec| spec.extensions && !spec.extensions.empty? end elsif options[:only_missing_extensions] - Gem::Specification.select(&:missing_extensions?) + specification_record.select(&:missing_extensions?) else get_all_gem_names.sort.map do |gem_name| - Gem::Specification.find_all_by_name(gem_name, options[:version]).reverse + specification_record.find_all_by_name(gem_name, options[:version]).reverse end.flatten end @@ -144,7 +148,7 @@ extensions will be restored. end unless spec.extensions.empty? || options[:extensions] || options[:only_executables] || options[:only_plugins] - say "Skipped #{spec.full_name}, it needs to compile an extension" + say "Skipped #{spec.full_name_with_location}, it needs to compile an extension" next end @@ -153,7 +157,7 @@ extensions will be restored. unless File.exist?(gem) || options[:only_executables] || options[:only_plugins] require_relative "../remote_fetcher" - say "Cached gem for #{spec.full_name} not found, attempting to fetch..." + say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..." dep = Gem::Dependency.new spec.name, spec.version found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep @@ -176,7 +180,6 @@ extensions will be restored. end bin_dir = options[:bin_dir] if options[:bin_dir] - install_dir = options[:install_dir] if options[:install_dir] installer_options = { wrappers: true, @@ -198,7 +201,7 @@ extensions will be restored. installer.install end - say "Restored #{spec.full_name}" + say "Restored #{spec.full_name_with_location}" end end end diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 3f38074280..bb2246ca31 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -279,11 +279,7 @@ By default, this RubyGems will install gem as: File.open bin_cmd_file, "w" do |file| file.puts <<-TEXT @ECHO OFF - IF NOT "%~f0" == "~f0" GOTO :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "#{dest_file}" %1 %2 %3 %4 %5 %6 %7 %8 %9 - GOTO :EOF - :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* + @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* TEXT end @@ -340,6 +336,8 @@ By default, this RubyGems will install gem as: require_relative "../rdoc" + return false unless defined?(Gem::RDoc) + fake_spec = Gem::Specification.new "rubygems", Gem::VERSION def fake_spec.full_gem_path File.expand_path "../../..", __dir__ @@ -585,6 +583,8 @@ abort "#{deprecation_message}" args = %w[--all --only-executables --silent] args << "--bindir=#{bindir}" + args << "--install-dir=#{default_dir}" + if options[:env_shebang] args << "--env-shebang" end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 2a77ec72cf..3d6e41e49e 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -157,9 +157,14 @@ that is a dependency of an existing gem. You can use the gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name]) - say("Gem '#{name}' is not installed") if gem_specs.empty? - gem_specs.each do |spec| - deplist.add spec + if gem_specs.empty? + say("Gem '#{name}' is not installed") + else + gem_specs.reject!(&:default_gem?) if gem_specs.size > 1 + + gem_specs.each do |spec| + deplist.add spec + end end end @@ -184,7 +189,7 @@ that is a dependency of an existing gem. You can use the rescue Gem::GemNotInHomeException => e spec = e.spec alert("In order to remove #{spec.name}, please execute:\n" \ - "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") + "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}") rescue Gem::UninstallError => e spec = e.spec alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \ diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 7874ad0dc9..a2bcb6dfbc 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -522,12 +522,12 @@ if you believe they were disclosed to a third party. # Return the configuration information for +key+. def [](key) - @hash[key.to_s] + @hash[key] || @hash[key.to_s] end # Set configuration option +key+ to +value+. def []=(key, value) - @hash[key.to_s] = value + @hash[key] = value end def ==(other) # :nodoc: @@ -555,8 +555,13 @@ if you believe they were disclosed to a third party. require_relative "yaml_serializer" content = Gem::YAMLSerializer.load(yaml) + deep_transform_config_keys!(content) + end - content.transform_keys! do |k| + private + + def self.deep_transform_config_keys!(config) + config.transform_keys! do |k| if k.match?(/\A:(.*)\Z/) k[1..-1].to_sym elsif k.include?("__") || k.match?(%r{/\Z}) @@ -570,7 +575,7 @@ if you believe they were disclosed to a third party. end end - content.transform_values! do |v| + config.transform_values! do |v| if v.is_a?(String) if v.match?(/\A:(.*)\Z/) v[1..-1].to_sym @@ -583,18 +588,18 @@ if you believe they were disclosed to a third party. else v end - elsif v.is_a?(Hash) && v.empty? + elsif v.empty? nil + elsif v.is_a?(Hash) + deep_transform_config_keys!(v) else v end end - content + config end - private - def set_config_file_name(args) @config_file_name = ENV["GEMRC"] need_config_file_name = false diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index d1bf074441..ecb4824d7e 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -271,15 +271,7 @@ class Gem::Dependency end def matching_specs(platform_only = false) - env_req = Gem.env_requirement(name) - matches = Gem::Specification.stubs_for(name).find_all do |spec| - requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) - end.map(&:to_spec) - - if prioritizes_bundler? - require_relative "bundler_version_finder" - Gem::BundlerVersionFinder.prioritize!(matches) - end + matches = Gem::Specification.find_all_by_name(name, requirement) if platform_only matches.reject! do |spec| @@ -297,10 +289,6 @@ class Gem::Dependency @requirement.specific? end - def prioritizes_bundler? - name == "bundler" && !specific? - end - def to_specs matches = matching_specs true @@ -349,4 +337,12 @@ class Gem::Dependency :released end end + + def encode_with(coder) # :nodoc: + coder.add "name", @name + coder.add "requirement", @requirement + coder.add "type", @type + coder.add "prerelease", @prerelease + coder.add "version_requirements", @version_requirements + end end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 0308b4687f..793324b875 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -104,9 +104,6 @@ end class Gem::GemNotFoundException < Gem::Exception; end -## -# Raised by the DependencyInstaller when a specific gem cannot be found - class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException ## # Creates a new SpecificGemNotFoundException for a gem with the given +name+ @@ -137,6 +134,8 @@ class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException attr_reader :errors end +Gem.deprecate_constant :SpecificGemNotFoundException + ## # Raised by Gem::Resolver when dependencies conflict and create the # inability to find a valid possible spec for a request. diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index be1ba3031c..12eb62ef16 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -19,13 +19,14 @@ class Gem::Ext::Builder $1.downcase end - def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"]) + def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"], + target_rbconfig: Gem.target_rbconfig) unless File.exist? File.join(make_dir, "Makefile") raise Gem::InstallError, "Makefile not found" end # try to find make program from Ruby configure arguments first - RbConfig::CONFIG["configure_args"] =~ /with-make-prog\=(\w+)/ + target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/ make_program_name = ENV["MAKE"] || ENV["make"] || $1 make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make" make_program = Shellwords.split(make_program_name) @@ -131,10 +132,11 @@ class Gem::Ext::Builder # have build arguments, saved, set +build_args+ which is an ARGV-style # array. - def initialize(spec, build_args = spec.build_args) + def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig) @spec = spec @build_args = build_args @gem_dir = spec.full_gem_path + @target_rbconfig = target_rbconfig @ran_rake = false end @@ -191,7 +193,7 @@ EOF FileUtils.mkdir_p dest_path results = builder.build(extension, dest_path, - results, @build_args, lib_dir, extension_dir) + results, @build_args, lib_dir, extension_dir, @target_rbconfig) verbose { results.join("\n") } diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 09ad1407c2..81b28c3c77 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -16,10 +16,15 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder @profile = :release end - def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd, + target_rbconfig=Gem.target_rbconfig) require "tempfile" require "fileutils" + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring" + end + # Where's the Cargo.toml of the crate we're building cargo_toml = File.join(cargo_dir, "Cargo.toml") # What's the crate's name diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index b162664784..34564f668d 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class Gem::Ext::CmakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd) + def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd, + target_rbconfig=Gem.target_rbconfig) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" + end + unless File.exist?(File.join(cmake_dir, "Makefile")) require_relative "../command" cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args] @@ -9,7 +14,7 @@ class Gem::Ext::CmakeBuilder < Gem::Ext::Builder run cmd, results, class_name, cmake_dir end - make dest_path, results, cmake_dir + make dest_path, results, cmake_dir, target_rbconfig: target_rbconfig results end diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index 6b8590ba2e..d91b1ec5e8 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -7,14 +7,19 @@ #++ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd) + def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd, + target_rbconfig=Gem.target_rbconfig) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring" + end + unless File.exist?(File.join(configure_dir, "Makefile")) cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args] run cmd, results, class_name, configure_dir end - make dest_path, results, configure_dir + make dest_path, results, configure_dir, target_rbconfig: target_rbconfig results end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index fb68a7a8cc..e652a221f8 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -7,7 +7,8 @@ #++ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd) + def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd, + target_rbconfig=Gem.target_rbconfig) require "fileutils" require "tempfile" @@ -23,6 +24,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder begin cmd = ruby << File.basename(extension) + cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path cmd.push(*args) run(cmd, results, class_name, extension_dir) do |s, r| @@ -39,11 +41,14 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder ENV["DESTDIR"] = nil - make dest_path, results, extension_dir, tmp_dest_relative + make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - if Gem.install_extension_in_lib && lib_dir + is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"] + # Do not copy extension libraries by default when cross-compiling + # not to conflict with the one already built for the host platform. + if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling FileUtils.mkdir_p lib_dir entries = Dir.entries(full_tmp_dest) - %w[. ..] entries = entries.map {|entry| File.join full_tmp_dest, entry } @@ -55,7 +60,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder destent.exist? || FileUtils.mv(ent.path, destent.path) end - make dest_path, results, extension_dir, tmp_dest_relative, ["clean"] + make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig ensure ENV["DESTDIR"] = destdir end diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 0171807b39..42393a4a06 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -9,7 +9,12 @@ require_relative "../shellwords" #++ class Gem::Ext::RakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd) + def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd, + target_rbconfig=Gem.target_rbconfig) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring" + end + if /mkrf_conf/i.match?(File.basename(extension)) run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir) end diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index aad207a718..0d0f0dc211 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -179,6 +179,11 @@ module Gem::InstallUpdateOptions "Suggest alternates when gems are not found") do |v,_o| options[:suggest_alternate] = v end + + add_option(:"Install/Update", "--target-rbconfig [FILE]", + "rbconfig.rb for the deployment target platform") do |v, _o| + Gem.set_target_rbconfig(v) + end end ## diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 8f6f9a5aa8..735d30ab9e 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -344,7 +344,7 @@ class Gem::Installer say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? - Gem::Specification.add_spec(spec) + Gem::Specification.add_spec(spec) unless @install_dir load_plugin @@ -500,8 +500,7 @@ class Gem::Installer dir_mode = options[:prog_mode] || (mode | 0o111) unless dir_mode == mode - require "fileutils" - FileUtils.chmod dir_mode, bin_path + File.chmod dir_mode, bin_path end check_executable_overwrite filename @@ -539,12 +538,14 @@ class Gem::Installer def generate_bin_script(filename, bindir) bin_script_path = File.join bindir, formatted_program_filename(filename) - require "fileutils" - FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers + Gem.open_file_with_lock(bin_script_path) do + require "fileutils" + FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers - File.open bin_script_path, "wb", 0o755 do |file| - file.print app_script_text(filename) - file.chmod(options[:prog_mode] || 0o755) + File.open(bin_script_path, "wb", 0o755) do |file| + file.write app_script_text(filename) + file.chmod(options[:prog_mode] || 0o755) + end end verbose bin_script_path @@ -847,7 +848,7 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, build_args + builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig builder.build_extensions end @@ -993,7 +994,7 @@ TEXT end def rb_config - RbConfig::CONFIG + Gem.target_rbconfig end def ruby_install_name diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 087f13f6c9..dd5e835a1e 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -95,14 +95,14 @@ class Gem::Package::TarHeader attr_reader(*FIELDS) - EMPTY_HEADER = ("\0" * 512).freeze # :nodoc: + EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc: ## # Creates a tar header from IO +stream+ def self.from(stream) header = stream.read 512 - empty = (header == EMPTY_HEADER) + return EMPTY if header == EMPTY_HEADER fields = header.unpack UNPACK_FORMAT @@ -123,7 +123,7 @@ class Gem::Package::TarHeader devminor: strict_oct(fields.shift), prefix: fields.shift, - empty: empty + empty: false end def self.strict_oct(str) @@ -172,6 +172,22 @@ class Gem::Package::TarHeader @empty = vals[:empty] end + EMPTY = new({ # :nodoc: + checksum: 0, + gname: "", + linkname: "", + magic: "", + mode: 0, + name: "", + prefix: "", + size: 0, + uname: "", + version: 0, + + empty: true, + }).freeze + private_constant :EMPTY + ## # Is the tar entry empty? @@ -241,7 +257,7 @@ class Gem::Package::TarHeader header = header.pack PACK_FORMAT - header << ("\0" * ((512 - header.size) % 512)) + header.ljust 512, "\0" end def oct(num, len) diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index d54ad12880..450c214167 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -12,9 +12,10 @@ class Gem::Platform attr_accessor :cpu, :os, :version - def self.local - @local ||= begin - arch = RbConfig::CONFIG["arch"] + def self.local(refresh: false) + return @local if @local && !refresh + @local = begin + arch = Gem.target_rbconfig["arch"] arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch) new(arch) end @@ -176,7 +177,7 @@ class Gem::Platform # they have the same version, or either one has no version # # Additionally, the platform will match if the local CPU is 'arm' and the - # other CPU starts with "arm" (for generic ARM family support). + # other CPU starts with "armv" (for generic 32-bit ARM family support). # # Of note, this method is not commutative. Indeed the OS 'linux' has a # special case: the version is the libc name, yet while "no version" stands @@ -197,7 +198,7 @@ class Gem::Platform # cpu ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu || - (@cpu == "arm" && other.cpu.start_with?("arm"))) && + (@cpu == "arm" && other.cpu.start_with?("armv"))) && # os @os == other.os && diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb index a95a759401..ea05969422 100644 --- a/lib/rubygems/query_utils.rb +++ b/lib/rubygems/query_utils.rb @@ -132,7 +132,7 @@ module Gem::QueryUtils version_matches = show_prereleases? || !s.version.prerelease? name_matches && version_matches - end + end.uniq(&:full_name) spec_tuples = specs.map do |spec| [spec.name_tuple, spec] diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index c3a41592f6..4b5c74e0ea 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -75,7 +75,6 @@ class Gem::RemoteFetcher def initialize(proxy=nil, dns=nil, headers={}) require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled require_relative "vendored_net_http" - require "stringio" require_relative "vendor/uri/lib/uri" Socket.do_not_reverse_lookup = true diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index 02543cb14a..d9796c4208 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -13,8 +13,8 @@ class Gem::Requirement OPS = { # :nodoc: "=" => lambda {|v, r| v == r }, "!=" => lambda {|v, r| v != r }, - ">" => lambda {|v, r| v > r }, - "<" => lambda {|v, r| v < r }, + ">" => lambda {|v, r| v > r }, + "<" => lambda {|v, r| v < r }, ">=" => lambda {|v, r| v >= r }, "<=" => lambda {|v, r| v <= r }, "~>" => lambda {|v, r| v >= r && v.release < r.bump }, @@ -214,10 +214,6 @@ class Gem::Requirement yaml_initialize coder.tag, coder.map end - def to_yaml_properties # :nodoc: - ["@requirements"] - end - def encode_with(coder) # :nodoc: coder.add "requirements", @requirements end diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb index fc9ff58f57..5c722001b1 100644 --- a/lib/rubygems/resolver/activation_request.rb +++ b/lib/rubygems/resolver/activation_request.rb @@ -106,7 +106,7 @@ class Gem::Resolver::ActivationRequest this_spec = full_spec Gem::Specification.any? do |s| - s == this_spec + s == this_spec && s.base_dir == this_spec.base_dir end end end diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index a983f8c6b6..57d0d00375 100644 --- a/lib/rubygems/resolver/best_set.rb +++ b/lib/rubygems/resolver/best_set.rb @@ -29,10 +29,8 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet pick_sets if @remote && @sets.empty? super - rescue Gem::RemoteFetcher::FetchError => e - replace_failed_api_set e - - retry + rescue Gem::RemoteFetcher::FetchError + [] end def prefetch(reqs) # :nodoc: @@ -50,28 +48,4 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet q.pp @sets end end - - ## - # Replaces a failed APISet for the URI in +error+ with an IndexSet. - # - # If no matching APISet can be found the original +error+ is raised. - # - # The calling method must retry the exception to repeat the lookup. - - def replace_failed_api_set(error) # :nodoc: - uri = error.original_uri - uri = Gem::URI uri unless Gem::URI === uri - uri += "." - - raise error unless api_set = @sets.find do |set| - Gem::Resolver::APISet === set && set.dep_uri == uri - end - - index_set = Gem::Resolver::IndexSet.new api_set.source - - @sets.map! do |set| - next set unless set == api_set - index_set - end - end end diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index d90e311b65..bee5681dab 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -79,7 +79,7 @@ class Gem::Source uri end - bundler_api_uri = enforce_trailing_slash(fetch_uri) + bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions" begin fetcher = Gem::RemoteFetcher.fetcher @@ -213,14 +213,16 @@ class Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[Remote:", "]" do - q.breakable - q.text @uri.to_s - - if api = uri + q.object_group(self) do + q.group 2, "[Remote:", "]" do q.breakable - q.text "API URI: " - q.text api.to_s + q.text @uri.to_s + + if api = uri + q.breakable + q.text "API URI: " + q.text api.to_s + end end end end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index bda63c6844..34f6851bc4 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -157,12 +157,14 @@ class Gem::Source::Git < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[Git: ", "]" do - q.breakable - q.text @repository + q.object_group(self) do + q.group 2, "[Git: ", "]" do + q.breakable + q.text @repository - q.breakable - q.text @reference + q.breakable + q.text @reference + end end end diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb index cbe12a0516..f5c96fee51 100644 --- a/lib/rubygems/source/installed.rb +++ b/lib/rubygems/source/installed.rb @@ -32,6 +32,8 @@ class Gem::Source::Installed < Gem::Source end def pretty_print(q) # :nodoc: - q.text "[Installed]" + q.object_group(self) do + q.text "[Installed]" + end end end diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index d81d8343a8..ba6eea1f9a 100644 --- a/lib/rubygems/source/local.rb +++ b/lib/rubygems/source/local.rb @@ -117,10 +117,14 @@ class Gem::Source::Local < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[Local gems:", "]" do - q.breakable - q.seplist @specs.keys do |v| - q.text v.full_name + q.object_group(self) do + q.group 2, "[Local gems:", "]" do + q.breakable + if @specs + q.seplist @specs.keys do |v| + q.text v.full_name + end + end end end end diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb index e9b2753646..dde1d48a21 100644 --- a/lib/rubygems/source/specific_file.rb +++ b/lib/rubygems/source/specific_file.rb @@ -42,9 +42,11 @@ class Gem::Source::SpecificFile < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[SpecificFile:", "]" do - q.breakable - q.text @path + q.object_group(self) do + q.group 2, "[SpecificFile:", "]" do + q.breakable + q.text @path + end end end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index a1eaf1248e..5735038ad3 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -11,6 +11,7 @@ require_relative "deprecate" require_relative "basic_specification" require_relative "stub_specification" require_relative "platform" +require_relative "specification_record" require_relative "util/list" require "rbconfig" @@ -174,24 +175,14 @@ class Gem::Specification < Gem::BasicSpecification end @@attributes = @@default_value.keys.sort_by(&:to_s) - @@array_attributes = @@default_value.reject {|_k,v| v != [] }.keys + @@array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k| @@default_value[k].nil? end - def self.clear_specs # :nodoc: - @@all = nil - @@stubs = nil - @@stubs_by_name = {} - @@spec_with_requirable_file = {} - @@active_stub_with_requirable_file = {} - end - private_class_method :clear_specs - - clear_specs - # Sentinel object to represent "not found" stubs NOT_FOUND = Struct.new(:to_spec, :this).new # :nodoc: + deprecate_constant :NOT_FOUND # Tracking removed method calls to warn users during build time. REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc: @@ -555,9 +546,9 @@ class Gem::Specification < Gem::BasicSpecification # # Usage: # - # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4' + # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4' - def add_runtime_dependency(gem, *requirements) + def add_dependency(gem, *requirements) if requirements.uniq.size != requirements.size warn "WARNING: duplicated #{gem} dependency #{requirements}" end @@ -770,7 +761,7 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :specification_version def self._all # :nodoc: - @@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec) + specification_record.all end def self.clear_load_cache # :nodoc: @@ -780,6 +771,11 @@ class Gem::Specification < Gem::BasicSpecification end private_class_method :clear_load_cache + def self.gem_path # :nodoc: + Gem.path + end + private_class_method :gem_path + def self.each_gemspec(dirs) # :nodoc: dirs.each do |dir| Gem::Util.glob_files_in_dir("*.gemspec", dir).each do |path| @@ -788,26 +784,9 @@ class Gem::Specification < Gem::BasicSpecification end end - def self.gemspec_stubs_in(dir, pattern) + def self.gemspec_stubs_in(dir, pattern) # :nodoc: Gem::Util.glob_files_in_dir(pattern, dir).map {|path| yield path }.select(&:valid?) end - private_class_method :gemspec_stubs_in - - def self.installed_stubs(dirs, pattern) - map_stubs(dirs, pattern) do |path, base_dir, gems_dir| - Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) - end - end - private_class_method :installed_stubs - - def self.map_stubs(dirs, pattern) # :nodoc: - dirs.flat_map do |dir| - base_dir = File.dirname dir - gems_dir = File.join base_dir, "gems" - gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir } - end - end - private_class_method :map_stubs def self.each_spec(dirs) # :nodoc: each_gemspec(dirs) do |path| @@ -820,13 +799,7 @@ class Gem::Specification < Gem::BasicSpecification # Returns a Gem::StubSpecification for every installed gem def self.stubs - @@stubs ||= begin - pattern = "*.gemspec" - stubs = stubs_for_pattern(pattern, false) - - @@stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name) - stubs - end + specification_record.stubs end ## @@ -845,13 +818,7 @@ class Gem::Specification < Gem::BasicSpecification # only returns stubs that match Gem.platforms def self.stubs_for(name) - if @@stubs - @@stubs_by_name[name] || [] - else - @@stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s| - s.name == name - end - end + specification_record.stubs_for(name) end ## @@ -859,12 +826,7 @@ class Gem::Specification < Gem::BasicSpecification # optionally filtering out specs not matching the current platform # def self.stubs_for_pattern(pattern, match_platform = true) # :nodoc: - installed_stubs = installed_stubs(Gem::Specification.dirs, pattern) - installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform - stubs = installed_stubs + default_stubs(pattern) - stubs = stubs.uniq(&:full_name) - _resort!(stubs) - stubs + specification_record.stubs_for_pattern(pattern, match_platform) end def self._resort!(specs) # :nodoc: @@ -873,7 +835,11 @@ class Gem::Specification < Gem::BasicSpecification next names if names.nonzero? versions = b.version <=> a.version next versions if versions.nonzero? - Gem::Platform.sort_priority(b.platform) + platforms = Gem::Platform.sort_priority(b.platform) <=> Gem::Platform.sort_priority(a.platform) + next platforms if platforms.nonzero? + default_gem = a.default_gem_priority <=> b.default_gem_priority + next default_gem if default_gem.nonzero? + a.base_dir_priority(gem_path) <=> b.base_dir_priority(gem_path) end end @@ -893,23 +859,14 @@ class Gem::Specification < Gem::BasicSpecification # properly sorted. def self.add_spec(spec) - return if _all.include? spec - - _all << spec - stubs << spec - (@@stubs_by_name[spec.name] ||= []) << spec - - _resort!(@@stubs_by_name[spec.name]) - _resort!(stubs) + specification_record.add_spec(spec) end ## # Removes +spec+ from the known specs. def self.remove_spec(spec) - _all.delete spec.to_spec - stubs.delete spec - (@@stubs_by_name[spec.name] || []).delete spec + specification_record.remove_spec(spec) end ## @@ -917,33 +874,23 @@ class Gem::Specification < Gem::BasicSpecification # You probably want to use one of the Enumerable methods instead. def self.all - warn "NOTE: Specification.all called from #{caller.first}" unless + warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless Gem::Deprecate.skip _all end ## - # Sets the known specs to +specs+. Not guaranteed to work for you in - # the future. Use at your own risk. Caveat emptor. Doomy doom doom. - # Etc etc. - # - #-- - # Makes +specs+ the known specs - # Listen, time is a river - # Winter comes, code breaks - # - # -- wilsonb + # Sets the known specs to +specs+. def self.all=(specs) - @@stubs_by_name = specs.group_by(&:name) - @@all = @@stubs = specs + specification_record.all = specs end ## # Return full names of all specs in sorted order. def self.all_names - _all.map(&:full_name) + specification_record.all_names end ## @@ -968,9 +915,7 @@ class Gem::Specification < Gem::BasicSpecification # Return the directories that Specification uses to find specs. def self.dirs - @@dirs ||= Gem.path.collect do |dir| - File.join dir, "specifications" - end + @@dirs ||= Gem::SpecificationRecord.dirs_from(gem_path) end ## @@ -980,7 +925,7 @@ class Gem::Specification < Gem::BasicSpecification def self.dirs=(dirs) reset - @@dirs = Array(dirs).map {|dir| File.join dir, "specifications" } + @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs)) end extend Enumerable @@ -989,21 +934,15 @@ class Gem::Specification < Gem::BasicSpecification # Enumerate every known spec. See ::dirs= and ::add_spec to set the list of # specs. - def self.each - return enum_for(:each) unless block_given? - - _all.each do |x| - yield x - end + def self.each(&block) + specification_record.each(&block) end ## # Returns every spec that matches +name+ and optional +requirements+. def self.find_all_by_name(name, *requirements) - requirements = Gem::Requirement.default if requirements.empty? - - Gem::Dependency.new(name, *requirements).matching_specs + specification_record.find_all_by_name(name, *requirements) end ## @@ -1033,12 +972,7 @@ class Gem::Specification < Gem::BasicSpecification # Return the best specification that contains the file matching +path+. def self.find_by_path(path) - path = path.dup.freeze - spec = @@spec_with_requirable_file[path] ||= stubs.find do |s| - s.contains_requirable_file? path - end || NOT_FOUND - - spec.to_spec + specification_record.find_by_path(path) end ## @@ -1046,19 +980,15 @@ class Gem::Specification < Gem::BasicSpecification # amongst the specs that are not activated. def self.find_inactive_by_path(path) - stub = stubs.find do |s| - next if s.activated? - s.contains_requirable_file? path - end - stub&.to_spec + specification_record.find_inactive_by_path(path) end - def self.find_active_stub_by_path(path) - stub = @@active_stub_with_requirable_file[path] ||= stubs.find do |s| - s.activated? && s.contains_requirable_file?(path) - end || NOT_FOUND + ## + # Return the best specification that contains the file matching +path+, among + # those already activated. - stub.this + def self.find_active_stub_by_path(path) + specification_record.find_active_stub_by_path(path) end ## @@ -1125,14 +1055,14 @@ class Gem::Specification < Gem::BasicSpecification # +prerelease+ is true. def self.latest_specs(prerelease = false) - _latest_specs Gem::Specification.stubs, prerelease + specification_record.latest_specs(prerelease) end ## # Return the latest installed spec for gem +name+. def self.latest_spec_for(name) - latest_specs(true).find {|installed_spec| installed_spec.name == name } + specification_record.latest_spec_for(name) end def self._latest_specs(specs, prerelease = false) # :nodoc: @@ -1270,7 +1200,7 @@ class Gem::Specification < Gem::BasicSpecification def self.reset @@dirs = nil Gem.pre_reset_hooks.each(&:call) - clear_specs + @specification_record = nil clear_load_cache unresolved = unresolved_deps unless unresolved.empty? @@ -1291,6 +1221,13 @@ class Gem::Specification < Gem::BasicSpecification Gem.post_reset_hooks.each(&:call) end + ## + # Keeps track of all currently known specifications + + def self.specification_record + @specification_record ||= Gem::SpecificationRecord.new(dirs) + end + # DOC: This method needs documented or nodoc'd def self.unresolved_deps @unresolved_deps ||= Hash.new {|h, n| h[n] = Gem::Dependency.new n } @@ -1381,7 +1318,7 @@ class Gem::Specification < Gem::BasicSpecification spec.instance_variable_set :@has_rdoc, array[15] spec.instance_variable_set :@new_platform, array[16] spec.instance_variable_set :@platform, array[16].to_s - spec.instance_variable_set :@license, array[17] + spec.instance_variable_set :@licenses, [array[17]] spec.instance_variable_set :@metadata, array[18] spec.instance_variable_set :@loaded, false spec.instance_variable_set :@activated, false @@ -1567,7 +1504,7 @@ class Gem::Specification < Gem::BasicSpecification private :add_dependency_with_type - alias_method :add_dependency, :add_runtime_dependency + alias_method :add_runtime_dependency, :add_dependency ## # Adds this spec's require paths to LOAD_PATH, in the proper location. @@ -1976,7 +1913,8 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Work around bundler removing my methods + # Work around old bundler versions removing my methods + # Can be removed once RubyGems can no longer install Bundler 2.5 def gem_dir # :nodoc: super @@ -2534,7 +2472,7 @@ class Gem::Specification < Gem::BasicSpecification if @installed_by_version result << nil - result << " s.installed_by_version = #{ruby_code Gem::VERSION} if s.respond_to? :installed_by_version" + result << " s.installed_by_version = #{ruby_code Gem::VERSION}" end unless dependencies.empty? @@ -2649,6 +2587,10 @@ class Gem::Specification < Gem::BasicSpecification @test_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) } end + def validate_for_resolution + Gem::SpecificationPolicy.new(self).validate_for_resolution + end + def validate_metadata Gem::SpecificationPolicy.new(self).validate_metadata end diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 812b0f889e..d79ee7df92 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -45,6 +45,7 @@ class Gem::SpecificationPolicy def validate(strict = false) validate_required! + validate_required_metadata! validate_optional(strict) if packaging || strict @@ -85,15 +86,17 @@ class Gem::SpecificationPolicy validate_authors_field - validate_metadata - validate_licenses_length - validate_lazy_metadata - validate_duplicate_dependencies end + def validate_required_metadata! + validate_metadata + + validate_lazy_metadata + end + def validate_optional(strict) validate_licenses @@ -121,6 +124,13 @@ class Gem::SpecificationPolicy end ## + # Implementation for Specification#validate_for_resolution + + def validate_for_resolution + validate_required! + end + + ## # Implementation for Specification#validate_metadata def validate_metadata @@ -297,7 +307,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: elsif !VALID_NAME_PATTERN.match?(name) error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores" elsif SPECIAL_CHARACTERS.match?(name) - error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore" + error "invalid value for attribute name: #{name.dump} cannot begin with a period, dash, or underscore" end end diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb new file mode 100644 index 0000000000..664d506265 --- /dev/null +++ b/lib/rubygems/specification_record.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +module Gem + class SpecificationRecord + def self.dirs_from(paths) + paths.map do |path| + File.join(path, "specifications") + end + end + + def self.from_path(path) + new(dirs_from([path])) + end + + def initialize(dirs) + @all = nil + @stubs = nil + @stubs_by_name = {} + @spec_with_requirable_file = {} + @active_stub_with_requirable_file = {} + + @dirs = dirs + end + + # Sentinel object to represent "not found" stubs + NOT_FOUND = Struct.new(:to_spec, :this).new + private_constant :NOT_FOUND + + ## + # Returns the list of all specifications in the record + + def all + @all ||= Gem.loaded_specs.values | stubs.map(&:to_spec) + end + + ## + # Returns a Gem::StubSpecification for every specification in the record + + def stubs + @stubs ||= begin + pattern = "*.gemspec" + stubs = stubs_for_pattern(pattern, false) + + @stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name) + stubs + end + end + + ## + # Returns a Gem::StubSpecification for every specification in the record + # named +name+ only returns stubs that match Gem.platforms + + def stubs_for(name) + if @stubs + @stubs_by_name[name] || [] + else + @stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s| + s.name == name + end + end + end + + ## + # Finds stub specifications matching a pattern in the record, optionally + # filtering out specs not matching the current platform + + def stubs_for_pattern(pattern, match_platform = true) + installed_stubs = installed_stubs(pattern) + installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform + stubs = installed_stubs + Gem::Specification.default_stubs(pattern) + Gem::Specification._resort!(stubs) + stubs + end + + ## + # Adds +spec+ to the the record, keeping the collection properly sorted. + + def add_spec(spec) + return if all.include? spec + + all << spec + stubs << spec + (@stubs_by_name[spec.name] ||= []) << spec + + Gem::Specification._resort!(@stubs_by_name[spec.name]) + Gem::Specification._resort!(stubs) + end + + ## + # Removes +spec+ from the record. + + def remove_spec(spec) + all.delete spec.to_spec + stubs.delete spec + (@stubs_by_name[spec.name] || []).delete spec + end + + ## + # Sets the specs known by the record to +specs+. + + def all=(specs) + @stubs_by_name = specs.group_by(&:name) + @all = @stubs = specs + end + + ## + # Return full names of all specs in the record in sorted order. + + def all_names + all.map(&:full_name) + end + + include Enumerable + + ## + # Enumerate every known spec. + + def each + return enum_for(:each) unless block_given? + + all.each do |x| + yield x + end + end + + ## + # Returns every spec in the record that matches +name+ and optional +requirements+. + + def find_all_by_name(name, *requirements) + req = Gem::Requirement.create(*requirements) + env_req = Gem.env_requirement(name) + + matches = stubs_for(name).find_all do |spec| + req.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) + end.map(&:to_spec) + + if name == "bundler" && !req.specific? + require_relative "bundler_version_finder" + Gem::BundlerVersionFinder.prioritize!(matches) + end + + matches + end + + ## + # Return the best specification in the record that contains the file matching +path+. + + def find_by_path(path) + path = path.dup.freeze + spec = @spec_with_requirable_file[path] ||= stubs.find do |s| + s.contains_requirable_file? path + end || NOT_FOUND + + spec.to_spec + end + + ## + # Return the best specification in the record that contains the file + # matching +path+ amongst the specs that are not activated. + + def find_inactive_by_path(path) + stub = stubs.find do |s| + next if s.activated? + s.contains_requirable_file? path + end + stub&.to_spec + end + + ## + # Return the best specification in the record that contains the file + # matching +path+, among those already activated. + + def find_active_stub_by_path(path) + stub = @active_stub_with_requirable_file[path] ||= stubs.find do |s| + s.activated? && s.contains_requirable_file?(path) + end || NOT_FOUND + + stub.this + end + + ## + # Return the latest specs in the record, optionally including prerelease + # specs if +prerelease+ is true. + + def latest_specs(prerelease) + Gem::Specification._latest_specs stubs, prerelease + end + + ## + # Return the latest installed spec in the record for gem +name+. + + def latest_spec_for(name) + latest_specs(true).find {|installed_spec| installed_spec.name == name } + end + + private + + def installed_stubs(pattern) + map_stubs(pattern) do |path, base_dir, gems_dir| + Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) + end + end + + def map_stubs(pattern) + @dirs.flat_map do |dir| + base_dir = File.dirname dir + gems_dir = File.join base_dir, "gems" + Gem::Specification.gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir } + end + end + end +end diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb index 58748df5d6..ea66fbc3f6 100644 --- a/lib/rubygems/stub_specification.rb +++ b/lib/rubygems/stub_specification.rb @@ -210,4 +210,25 @@ class Gem::StubSpecification < Gem::BasicSpecification def stubbed? data.is_a? StubLine end + + def ==(other) # :nodoc: + self.class === other && + name == other.name && + version == other.version && + platform == other.platform + end + + alias_method :eql?, :== # :nodoc: + + def hash # :nodoc: + name.hash ^ version.hash ^ platform.hash + end + + def <=>(other) # :nodoc: + sort_obj <=> other.sort_obj + end + + def sort_obj # :nodoc: + [name, version, Gem::Platform.sort_priority(platform)] + end end diff --git a/lib/rubygems/target_rbconfig.rb b/lib/rubygems/target_rbconfig.rb new file mode 100644 index 0000000000..21d90ee9db --- /dev/null +++ b/lib/rubygems/target_rbconfig.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rbconfig" + +## +# A TargetConfig is a wrapper around an RbConfig object that provides a +# consistent interface for querying configuration for *deployment target +# platform*, where the gem being installed is intended to run on. +# +# The TargetConfig is typically created from the RbConfig of the running Ruby +# process, but can also be created from an RbConfig file on disk for cross- +# compiling gems. + +class Gem::TargetRbConfig + attr_reader :path + + def initialize(rbconfig, path) + @rbconfig = rbconfig + @path = path + end + + ## + # Creates a TargetRbConfig for the platform that RubyGems is running on. + + def self.for_running_ruby + new(::RbConfig, nil) + end + + ## + # Creates a TargetRbConfig from the RbConfig file at the given path. + # Typically used for cross-compiling gems. + + def self.from_path(rbconfig_path) + namespace = Module.new do |m| + # Load the rbconfig.rb file within a new anonymous module to avoid + # conflicts with the rbconfig for the running platform. + Kernel.load rbconfig_path, m + end + rbconfig = namespace.const_get(:RbConfig) + + new(rbconfig, rbconfig_path) + end + + ## + # Queries the configuration for the given key. + + def [](key) + @rbconfig::CONFIG[key] + end +end diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index c96df2a085..471c29b6e4 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -32,7 +32,7 @@ class Gem::Uninstaller attr_reader :bin_dir ## - # The gem repository the gem will be installed into + # The gem repository the gem will be uninstalled from attr_reader :gem_home @@ -49,8 +49,9 @@ class Gem::Uninstaller # TODO: document the valid options @gem = gem @version = options[:version] || Gem::Requirement.default - @gem_home = File.realpath(options[:install_dir] || Gem.dir) - @plugins_dir = Gem.plugindir(@gem_home) + @install_dir = options[:install_dir] + @gem_home = File.realpath(@install_dir || Gem.dir) + @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir @force_executables = options[:executables] @force_all = options[:all] @force_ignore = options[:ignore] @@ -70,7 +71,7 @@ class Gem::Uninstaller # only add user directory if install_dir is not set @user_install = false - @user_install = options[:user_install] unless options[:install_dir] + @user_install = options[:user_install] unless @install_dir # Optimization: populated during #uninstall @default_specs_matching_uninstall_params = [] @@ -85,11 +86,7 @@ class Gem::Uninstaller list = [] - dirs = - Gem::Specification.dirs + - [Gem.default_specifications_dir] - - Gem::Specification.each_spec dirs do |spec| + specification_record.stubs.each do |spec| next unless dependency.matches_spec? spec list << spec @@ -101,11 +98,11 @@ class Gem::Uninstaller default_specs, list = list.partition(&:default_gem?) warn_cannot_uninstall_default_gems(default_specs - list) - @default_specs_matching_uninstall_params = default_specs + @default_specs_matching_uninstall_params = default_specs.map(&:to_spec) list, other_repo_specs = list.partition do |spec| @gem_home == spec.base_dir || - (@user_install && spec.base_dir == Gem.user_dir) + (@user_install && spec.base_dir == @user_dir) end list.sort! @@ -125,7 +122,7 @@ class Gem::Uninstaller remove_all list elsif list.size > 1 - gem_names = list.map(&:full_name) + gem_names = list.map(&:full_name_with_location) gem_names << "All versions" say @@ -146,7 +143,9 @@ class Gem::Uninstaller ## # Uninstalls gem +spec+ - def uninstall_gem(spec) + def uninstall_gem(stub) + spec = stub.to_spec + @spec = spec unless dependencies_ok? spec @@ -164,6 +163,8 @@ class Gem::Uninstaller remove_plugins @spec remove @spec + specification_record.remove_spec(stub) + regenerate_plugins Gem.post_uninstall_hooks.each do |hook| @@ -177,7 +178,7 @@ class Gem::Uninstaller # Removes installed executables and batch files (windows only) for +spec+. def remove_executables(spec) - return if spec.executables.empty? + return if spec.executables.empty? || default_spec_matches?(spec) executables = spec.executables.clone @@ -239,7 +240,7 @@ class Gem::Uninstaller def remove(spec) unless path_ok?(@gem_home, spec) || - (@user_install && path_ok?(Gem.user_dir, spec)) + (@user_install && path_ok?(@user_dir, spec)) e = Gem::GemNotInHomeException.new \ "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}" e.spec = spec @@ -250,7 +251,15 @@ class Gem::Uninstaller raise Gem::FilePermissionError, spec.base_dir unless File.writable?(spec.base_dir) - safe_delete { FileUtils.rm_r spec.full_gem_path } + full_gem_path = spec.full_gem_path + exclusions = [] + + if default_spec_matches?(spec) && spec.executables.any? + exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) } + exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path + end + + safe_delete { rm_r full_gem_path, exclusions: exclusions } safe_delete { FileUtils.rm_r spec.extension_dir } old_platform_name = spec.original_name @@ -274,8 +283,6 @@ class Gem::Uninstaller safe_delete { FileUtils.rm_r gemspec } announce_deletion_of(spec) - - Gem::Specification.reset end ## @@ -284,17 +291,17 @@ class Gem::Uninstaller def remove_plugins(spec) # :nodoc: return if spec.plugins.empty? - remove_plugins_for(spec, @plugins_dir) + remove_plugins_for(spec, plugin_dir_for(spec)) end ## # Regenerates plugin wrappers after removal. def regenerate_plugins - latest = Gem::Specification.latest_spec_for(@spec.name) + latest = specification_record.latest_spec_for(@spec.name) return if latest.nil? - regenerate_plugins_for(latest, @plugins_dir) + regenerate_plugins_for(latest, plugin_dir_for(@spec)) end ## @@ -379,6 +386,16 @@ class Gem::Uninstaller private + def rm_r(path, exclusions:) + FileUtils::Entry_.new(path).postorder_traverse do |ent| + ent.remove unless exclusions.include?(ent.path) + end + end + + def specification_record + @specification_record ||= @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record + end + def announce_deletion_of(spec) name = spec.full_name say "Successfully uninstalled #{name}" @@ -406,4 +423,8 @@ class Gem::Uninstaller say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem" end end + + def plugin_dir_for(spec) + Gem.plugindir(spec.base_dir) + end end diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb index f3c7201639..192ae30b9b 100644 --- a/lib/rubygems/util/licenses.rb +++ b/lib/rubygems/util/licenses.rb @@ -15,6 +15,7 @@ class Gem::Licenses # license identifiers LICENSE_IDENTIFIERS = %w[ 0BSD + 3D-Slicer-1.0 AAL ADSL AFL-1.1 @@ -26,6 +27,7 @@ class Gem::Licenses AGPL-1.0-or-later AGPL-3.0-only AGPL-3.0-or-later + AMD-newlib AMDPLPA AML AML-glslang @@ -62,6 +64,7 @@ class Gem::Licenses BSD-2-Clause-Darwin BSD-2-Clause-Patent BSD-2-Clause-Views + BSD-2-Clause-first-lines BSD-3-Clause BSD-3-Clause-Attribution BSD-3-Clause-Clear @@ -191,6 +194,7 @@ class Gem::Licenses CUA-OPL-1.0 Caldera Caldera-no-preamble + Catharon ClArtistic Clips Community-Spec-1.0 @@ -270,25 +274,32 @@ class Gem::Licenses Glide Glulxe Graphics-Gems + Gutmann HP-1986 HP-1989 HPND HPND-DEC HPND-Fenneberg-Livingston HPND-INRIA-IMAG + HPND-Intel HPND-Kevlin-Henney HPND-MIT-disclaimer HPND-Markus-Kuhn HPND-Pbmplus HPND-UC + HPND-UC-export-US HPND-doc HPND-doc-sell HPND-export-US + HPND-export-US-acknowledgement HPND-export-US-modify + HPND-export2-US + HPND-merchantability-variant HPND-sell-MIT-disclaimer-xserver HPND-sell-regexpr HPND-sell-variant HPND-sell-variant-MIT-disclaimer + HPND-sell-variant-MIT-disclaimer-rev HTMLTIDY HaskellReport Hippocratic-2.1 @@ -353,6 +364,7 @@ class Gem::Licenses MIT-0 MIT-CMU MIT-Festival + MIT-Khronos-old MIT-Modern-Variant MIT-Wu MIT-advertising @@ -386,7 +398,9 @@ class Gem::Licenses NAIST-2003 NASA-1.3 NBPL-1.0 + NCBI-PD NCGL-UK-2.0 + NCL NCSA NGPL NICTA-1.0 @@ -410,6 +424,7 @@ class Gem::Licenses Nokia Noweb O-UDA-1.0 + OAR OCCT-PL OCLC-2.0 ODC-By-1.0 @@ -463,6 +478,7 @@ class Gem::Licenses PDDL-1.0 PHP-3.0 PHP-3.01 + PPL PSF-2.0 Parity-6.0.0 Parity-7.0.0 @@ -518,6 +534,7 @@ class Gem::Licenses Spencer-99 SugarCRM-1.1.3 Sun-PPP + Sun-PPP-2000 SunPro Symlinks TAPR-OHL-1.0 @@ -574,6 +591,7 @@ class Gem::Licenses Zimbra-1.3 Zimbra-1.4 Zlib + any-OSI bcrypt-Solar-Designer blessing bzip2-1.0.6 @@ -582,6 +600,7 @@ class Gem::Licenses copyleft-next-0.3.0 copyleft-next-0.3.1 curl + cve-tou diffmark dtoa dvipdfm @@ -604,6 +623,7 @@ class Gem::Licenses mpi-permissive mpich2 mplus + pkgconf pnmstitch psfrag psutils @@ -613,12 +633,14 @@ class Gem::Licenses softSurfer ssh-keyscan swrule + threeparttable ulem w3m xinetd xkeyboard-config-Zinoviev xlock xpp + xzoom zlib-acknowledgement ].freeze @@ -660,6 +682,7 @@ class Gem::Licenses EXCEPTION_IDENTIFIERS = %w[ 389-exception Asterisk-exception + Asterisk-linking-protocols-exception Autoconf-exception-2.0 Autoconf-exception-3.0 Autoconf-exception-generic @@ -697,11 +720,13 @@ class Gem::Licenses OCCT-exception-1.0 OCaml-LGPL-linking-exception OpenJDK-assembly-exception-1.0 + PCRE2-exception PS-or-PDF-font-exception-20170817 QPL-1.0-INRIA-2004-exception Qt-GPL-exception-1.0 Qt-LGPL-exception-1.1 Qwt-exception-1.0 + RRDtool-FLOSS-exception-2.0 SANE-exception SHL-2.0 SHL-2.1 diff --git a/lib/rubygems/vendor/net-http/lib/net/https.rb b/lib/rubygems/vendor/net-http/lib/net/https.rb index d2784f0be0..f104c85c81 100644 --- a/lib/rubygems/vendor/net-http/lib/net/https.rb +++ b/lib/rubygems/vendor/net-http/lib/net/https.rb @@ -4,7 +4,7 @@ = net/https -- SSL/TLS enhancement for Gem::Net::HTTP. This file has been merged with net/http. There is no longer any need to - require 'rubygems/vendor/net-http/lib/net/https' to use HTTPS. + require_relative 'https' to use HTTPS. See Gem::Net::HTTP for details on how to make HTTPS connections. diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb index 5937431720..00dc7c8a67 100644 --- a/lib/rubygems/vendor/optparse/lib/optparse.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -1084,7 +1084,7 @@ XXX Switch::OptionalArgument.new do |pkg| if pkg begin - require 'rubygems/vendor/optparse/lib/optparse/version' + require_relative 'optparse/version' rescue LoadError else show_version(*pkg.split(/,/)) or diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb index ac0ba0b313..0f5ded3b76 100644 --- a/lib/rubygems/vendor/resolv/lib/resolv.rb +++ b/lib/rubygems/vendor/resolv/lib/resolv.rb @@ -5,7 +5,7 @@ require_relative '../../timeout/lib/timeout' require 'io/wait' begin - require 'securerandom' + require_relative '../../../vendored_securerandom' rescue LoadError end @@ -602,10 +602,10 @@ class Gem::Resolv } end - if defined? SecureRandom + if defined? Gem::SecureRandom def self.random(arg) # :nodoc: begin - SecureRandom.random_number(arg) + Gem::SecureRandom.random_number(arg) rescue NotImplementedError rand(arg) end diff --git a/lib/rubygems/vendor/securerandom/.document b/lib/rubygems/vendor/securerandom/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/securerandom/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/securerandom/lib/random/formatter.rb b/lib/rubygems/vendor/securerandom/lib/random/formatter.rb new file mode 100644 index 0000000000..3544033340 --- /dev/null +++ b/lib/rubygems/vendor/securerandom/lib/random/formatter.rb @@ -0,0 +1,373 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +# == \Random number formatter. +# +# Formats generated random numbers in many manners. When <tt>'random/formatter'</tt> +# is required, several methods are added to empty core module <tt>Gem::Random::Formatter</tt>, +# making them available as Random's instance and module methods. +# +# Standard library Gem::SecureRandom is also extended with the module, and the methods +# described below are available as a module methods in it. +# +# === Examples +# +# Generate random hexadecimal strings: +# +# require 'rubygems/vendor/securerandom/lib/random/formatter' +# +# prng = Random.new +# prng.hex(10) #=> "52750b30ffbc7de3b362" +# prng.hex(10) #=> "92b15d6c8dc4beb5f559" +# prng.hex(13) #=> "39b290146bea6ce975c37cfc23" +# # or just +# Random.hex #=> "1aed0c631e41be7f77365415541052ee" +# +# Generate random base64 strings: +# +# prng.base64(10) #=> "EcmTPZwWRAozdA==" +# prng.base64(10) #=> "KO1nIU+p9DKxGg==" +# prng.base64(12) #=> "7kJSM/MzBJI+75j8" +# Random.base64(4) #=> "bsQ3fQ==" +# +# Generate random binary strings: +# +# prng.random_bytes(10) #=> "\016\t{\370g\310pbr\301" +# prng.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337" +# Random.random_bytes(6) #=> "\xA1\xE6Lr\xC43" +# +# Generate alphanumeric strings: +# +# prng.alphanumeric(10) #=> "S8baxMJnPl" +# prng.alphanumeric(10) #=> "aOxAg8BAJe" +# Random.alphanumeric #=> "TmP9OsJHJLtaZYhP" +# +# Generate UUIDs: +# +# prng.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594" +# prng.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab" +# Random.uuid #=> "f14e0271-de96-45cc-8911-8910292a42cd" +# +# All methods are available in the standard library Gem::SecureRandom, too: +# +# Gem::SecureRandom.hex #=> "05b45376a30c67238eb93b16499e50cf" + +module Gem::Random::Formatter + + # Generate a random binary string. + # + # The argument _n_ specifies the length of the result string. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in future. + # + # The result may contain any byte: "\x00" - "\xff". + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6" + # # or + # prng = Random.new + # prng.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97" + def random_bytes(n=nil) + n = n ? n.to_int : 16 + gen_random(n) + end + + # Generate a random hexadecimal string. + # + # The argument _n_ specifies the length, in bytes, of the random number to be generated. + # The length of the resulting hexadecimal string is twice of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain 0-9 and a-f. + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.hex #=> "eb693ec8252cd630102fd0d0fb7c3485" + # # or + # prng = Random.new + # prng.hex #=> "91dc3bfb4de5b11d029d376634589b61" + def hex(n=nil) + random_bytes(n).unpack1("H*") + end + + # Generate a random base64 string. + # + # The argument _n_ specifies the length, in bytes, of the random number + # to be generated. The length of the result string is about 4/3 of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain A-Z, a-z, 0-9, "+", "/" and "=". + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A==" + # # or + # prng = Random.new + # prng.base64 #=> "6BbW0pxO0YENxn38HMUbcQ==" + # + # See RFC 3548 for the definition of base64. + def base64(n=nil) + [random_bytes(n)].pack("m0") + end + + # Generate a random URL-safe base64 string. + # + # The argument _n_ specifies the length, in bytes, of the random number + # to be generated. The length of the result string is about 4/3 of _n_. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The boolean argument _padding_ specifies the padding. + # If it is false or nil, padding is not generated. + # Otherwise padding is generated. + # By default, padding is not generated because "=" may be used as a URL delimiter. + # + # The result may contain A-Z, a-z, 0-9, "-" and "_". + # "=" is also used if _padding_ is true. + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg" + # # or + # prng = Random.new + # prng.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg" + # + # prng.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ==" + # prng.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg==" + # + # See RFC 3548 for the definition of URL-safe base64. + def urlsafe_base64(n=nil, padding=false) + s = [random_bytes(n)].pack("m0") + s.tr!("+/", "-_") + s.delete!("=") unless padding + s + end + + # Generate a random v4 UUID (Universally Unique IDentifier). + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594" + # Random.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab" + # # or + # prng = Random.new + # prng.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b" + # + # The version 4 UUID is purely random (except the version). + # It doesn't contain meaningful information such as MAC addresses, timestamps, etc. + # + # The result contains 122 random bits (15.25 random bytes). + # + # See RFC4122[https://datatracker.ietf.org/doc/html/rfc4122] for details of UUID. + # + def uuid + ary = random_bytes(16).unpack("NnnnnN") + ary[2] = (ary[2] & 0x0fff) | 0x4000 + ary[3] = (ary[3] & 0x3fff) | 0x8000 + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + alias uuid_v4 uuid + + # Generate a random v7 UUID (Universally Unique IDentifier). + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.uuid_v7 # => "0188d4c3-1311-7f96-85c7-242a7aa58f1e" + # Random.uuid_v7 # => "0188d4c3-16fe-744f-86af-38fa04c62bb5" + # Random.uuid_v7 # => "0188d4c3-1af8-764f-b049-c204ce0afa23" + # Random.uuid_v7 # => "0188d4c3-1e74-7085-b14f-ef6415dc6f31" + # # |<--sorted-->| |<----- random ---->| + # + # # or + # prng = Random.new + # prng.uuid_v7 # => "0188ca51-5e72-7950-a11d-def7ff977c98" + # + # The version 7 UUID starts with the least significant 48 bits of a 64 bit + # Unix timestamp (milliseconds since the epoch) and fills the remaining bits + # with random data, excluding the version and variant bits. + # + # This allows version 7 UUIDs to be sorted by creation time. Time ordered + # UUIDs can be used for better database index locality of newly inserted + # records, which may have a significant performance benefit compared to random + # data inserts. + # + # The result contains 74 random bits (9.25 random bytes). + # + # Note that this method cannot be made reproducable because its output + # includes not only random bits but also timestamp. + # + # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/] + # for details of UUIDv7. + # + # ==== Monotonicity + # + # UUIDv7 has millisecond precision by default, so multiple UUIDs created + # within the same millisecond are not issued in monotonically increasing + # order. To create UUIDs that are time-ordered with sub-millisecond + # precision, up to 12 bits of additional timestamp may added with + # +extra_timestamp_bits+. The extra timestamp precision comes at the expense + # of random bits. Setting <tt>extra_timestamp_bits: 12</tt> provides ~244ns + # of precision, but only 62 random bits (7.75 random bytes). + # + # prng = Random.new + # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 12) } + # # => + # ["0188d4c7-13da-74f9-8b53-22a786ffdd5a", + # "0188d4c7-13da-753b-83a5-7fb9b2afaeea", + # "0188d4c7-13da-754a-88ea-ac0baeedd8db", + # "0188d4c7-13da-7557-83e1-7cad9cda0d8d"] + # # |<--- sorted --->| |<-- random --->| + # + # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 8) } + # # => + # ["0188d4c7-3333-7a95-850a-de6edb858f7e", + # "0188d4c7-3333-7ae8-842e-bc3a8b7d0cf9", # <- out of order + # "0188d4c7-3333-7ae2-995a-9f135dc44ead", # <- out of order + # "0188d4c7-3333-7af9-87c3-8f612edac82e"] + # # |<--- sorted -->||<---- random --->| + # + # Any rollbacks of the system clock will break monotonicity. UUIDv7 is based + # on UTC, which excludes leap seconds and can rollback the clock. To avoid + # this, the system clock can synchronize with an NTP server configured to use + # a "leap smear" approach. NTP or PTP will also be needed to synchronize + # across distributed nodes. + # + # Counters and other mechanisms for stronger guarantees of monotonicity are + # not implemented. Applications with stricter requirements should follow + # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters] + # of the specification. + # + def uuid_v7(extra_timestamp_bits: 0) + case (extra_timestamp_bits = Integer(extra_timestamp_bits)) + when 0 # min timestamp precision + ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) + rand = random_bytes(10) + rand.setbyte(0, rand.getbyte(0) & 0x0f | 0x70) # version + rand.setbyte(2, rand.getbyte(2) & 0x3f | 0x80) # variant + "%08x-%04x-%s" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + rand.unpack("H4H4H12").join("-") + ] + + when 12 # max timestamp precision + ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .divmod(1_000_000) + extra_bits = ns * 4096 / 1_000_000 + rand = random_bytes(8) + rand.setbyte(0, rand.getbyte(0) & 0x3f | 0x80) # variant + "%08x-%04x-7%03x-%s" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + extra_bits, + rand.unpack("H4H12").join("-") + ] + + when (0..12) # the generic version is slower than the special cases above + rand_a, rand_b1, rand_b2, rand_b3 = random_bytes(10).unpack("nnnN") + rand_mask_bits = 12 - extra_timestamp_bits + ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + .divmod(1_000_000) + "%08x-%04x-%04x-%04x-%04x%08x" % [ + (ms & 0x0000_ffff_ffff_0000) >> 16, + (ms & 0x0000_0000_0000_ffff), + 0x7000 | + ((ns * (1 << extra_timestamp_bits) / 1_000_000) << rand_mask_bits) | + rand_a & ((1 << rand_mask_bits) - 1), + 0x8000 | (rand_b1 & 0x3fff), + rand_b2, + rand_b3 + ] + + else + raise ArgumentError, "extra_timestamp_bits must be in 0..12" + end + end + + # Internal interface to Random; Generate random data _n_ bytes. + private def gen_random(n) + self.bytes(n) + end + + # Generate a string that randomly draws from a + # source array of characters. + # + # The argument _source_ specifies the array of characters from which + # to generate the string. + # The argument _n_ specifies the length, in characters, of the string to be + # generated. + # + # The result may contain whatever characters are in the source array. + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # prng.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron" + # prng.choose([*'0'..'9'], 5) #=> "27309" + private def choose(source, n) + size = source.size + m = 1 + limit = size + while limit * size <= 0x100000000 + limit *= size + m += 1 + end + result = ''.dup + while m <= n + rs = random_number(limit) + is = rs.digits(size) + (m-is.length).times { is << 0 } + result << source.values_at(*is).join('') + n -= m + end + if 0 < n + rs = random_number(limit) + is = rs.digits(size) + if is.length < n + (n-is.length).times { is << 0 } + else + is.pop while n < is.length + end + result.concat source.values_at(*is).join('') + end + result + end + + # The default character list for #alphanumeric. + ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9'] + + # Generate a random alphanumeric string. + # + # The argument _n_ specifies the length, in characters, of the alphanumeric + # string to be generated. + # The argument _chars_ specifies the character list which the result is + # consist of. + # + # If _n_ is not specified or is nil, 16 is assumed. + # It may be larger in the future. + # + # The result may contain A-Z, a-z and 0-9, unless _chars_ is specified. + # + # require 'rubygems/vendor/securerandom/lib/random/formatter' + # + # Random.alphanumeric #=> "2BuBuLf3WfSKyQbR" + # # or + # prng = Random.new + # prng.alphanumeric(10) #=> "i6K93NdqiH" + # + # Random.alphanumeric(4, chars: [*"0".."9"]) #=> "2952" + # # or + # prng = Random.new + # prng.alphanumeric(10, chars: [*"!".."/"]) #=> ",.,++%/''." + def alphanumeric(n = nil, chars: ALPHANUMERIC) + n = 16 if n.nil? + choose(chars, n) + end +end diff --git a/lib/rubygems/vendor/securerandom/lib/securerandom.rb b/lib/rubygems/vendor/securerandom/lib/securerandom.rb new file mode 100644 index 0000000000..f83d2a74fc --- /dev/null +++ b/lib/rubygems/vendor/securerandom/lib/securerandom.rb @@ -0,0 +1,96 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +require_relative 'random/formatter' + +# == Secure random number generator interface. +# +# This library is an interface to secure random number generators which are +# suitable for generating session keys in HTTP cookies, etc. +# +# You can use this library in your application by requiring it: +# +# require 'rubygems/vendor/securerandom/lib/securerandom' +# +# It supports the following secure random number generators: +# +# * openssl +# * /dev/urandom +# * Win32 +# +# Gem::SecureRandom is extended by the Gem::Random::Formatter module which +# defines the following methods: +# +# * alphanumeric +# * base64 +# * choose +# * gen_random +# * hex +# * rand +# * random_bytes +# * random_number +# * urlsafe_base64 +# * uuid +# +# These methods are usable as class methods of Gem::SecureRandom such as +# +Gem::SecureRandom.hex+. +# +# If a secure random number generator is not available, +# +NotImplementedError+ is raised. + +module Gem::SecureRandom + + # The version + VERSION = "0.3.1" + + class << self + # Returns a random binary string containing +size+ bytes. + # + # See Random.bytes + def bytes(n) + return gen_random(n) + end + + private + + # :stopdoc: + + # Implementation using OpenSSL + def gen_random_openssl(n) + return OpenSSL::Random.random_bytes(n) + end + + # Implementation using system random device + def gen_random_urandom(n) + ret = Random.urandom(n) + unless ret + raise NotImplementedError, "No random device" + end + unless ret.length == n + raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes" + end + ret + end + + begin + # Check if Random.urandom is available + Random.urandom(1) + alias gen_random gen_random_urandom + rescue RuntimeError + begin + require 'openssl' + rescue NoMethodError + raise NotImplementedError, "No random device" + else + alias gen_random gen_random_openssl + end + end + + # :startdoc: + + # Generate random data bytes for Gem::Random::Formatter + public :gen_random + end +end + +Gem::SecureRandom.extend(Gem::Random::Formatter) diff --git a/lib/rubygems/vendored_securerandom.rb b/lib/rubygems/vendored_securerandom.rb new file mode 100644 index 0000000000..0ce26905c4 --- /dev/null +++ b/lib/rubygems/vendored_securerandom.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module Gem::Random; end +require_relative "vendor/securerandom/lib/securerandom" diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index e174d8ad95..d9cd91bffa 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -297,10 +297,6 @@ class Gem::Version @hash = nil end - def to_yaml_properties # :nodoc: - ["@version"] - end - def encode_with(coder) # :nodoc: coder.add "version", @version end diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 128becc1ce..f89004f32a 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -41,7 +41,7 @@ module Gem HASH_REGEX = / ^ ([ ]*) # indentations - (.+) # key + ([^#]+) # key excludes comment char '#' (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) [ ]? (['"]?) # optional opening quote @@ -60,7 +60,6 @@ module Gem indent, key, quote, val = match.captures val = strip_comment(val) - convert_to_backward_compatible_key!(key) depth = indent.size / 2 if quote.empty? && val.empty? new_hash = {} @@ -92,14 +91,8 @@ module Gem end end - # for settings' keys - def convert_to_backward_compatible_key!(key) - key << "/" if /https?:/i.match?(key) && !%r{/\Z}.match?(key) - key.gsub!(".", "__") - end - class << self - private :dump_hash, :convert_to_backward_compatible_key! + private :dump_hash end end end diff --git a/lib/set.rb b/lib/set.rb index a0954a31d1..8ac9ce45b1 100644 --- a/lib/set.rb +++ b/lib/set.rb @@ -3,7 +3,7 @@ # # set.rb - defines the Set class # -# Copyright (c) 2002-2023 Akinori MUSHA <knu@iDaemons.org> +# Copyright (c) 2002-2024 Akinori MUSHA <knu@iDaemons.org> # # Documentation by Akinori MUSHA and Gavin Sinclair. # @@ -335,7 +335,7 @@ class Set end end - # Converts the set to an array. The order of elements is uncertain. + # Returns an array containing all elements in the set. # # Set[1, 2].to_a #=> [1, 2] # Set[1, 'c', :s].to_a #=> [1, "c", :s] @@ -540,22 +540,22 @@ class Set # Deletes every element of the set for which block evaluates to # true, and returns self. Returns an enumerator if no block is # given. - def delete_if + def delete_if(&block) block_given? or return enum_for(__method__) { size } - # @hash.delete_if should be faster, but using it breaks the order - # of enumeration in subclasses. - select { |o| yield o }.each { |o| @hash.delete(o) } + # Instead of directly using @hash.delete_if, perform enumeration + # using self.each that subclasses may override. + select(&block).each { |o| @hash.delete(o) } self end # Deletes every element of the set for which block evaluates to # false, and returns self. Returns an enumerator if no block is # given. - def keep_if + def keep_if(&block) block_given? or return enum_for(__method__) { size } - # @hash.keep_if should be faster, but using it breaks the order of - # enumeration in subclasses. - reject { |o| yield o }.each { |o| @hash.delete(o) } + # Instead of directly using @hash.keep_if, perform enumeration + # using self.each that subclasses may override. + reject(&block).each { |o| @hash.delete(o) } self end diff --git a/lib/shellwords.gemspec b/lib/shellwords.gemspec index d60ab5f650..8d0c518ca5 100644 --- a/lib/shellwords.gemspec +++ b/lib/shellwords.gemspec @@ -21,8 +21,9 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + srcdir, gemspec_file = File.split(__FILE__) + spec.files = Dir.chdir(srcdir) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:(?:test|spec|features)/|\.git|Rake)}) || f == gemspec_file} end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } diff --git a/lib/shellwords.rb b/lib/shellwords.rb index 067432e374..d8243abd61 100644 --- a/lib/shellwords.rb +++ b/lib/shellwords.rb @@ -5,9 +5,9 @@ # This module manipulates strings according to the word parsing rules # of the UNIX Bourne shell. # -# The shellwords() function was originally a port of shellwords.pl, -# but modified to conform to the Shell & Utilities volume of the IEEE -# Std 1003.1-2008, 2016 Edition [1]. +# The <tt>shellwords()</tt> function was originally a port of shellwords.pl, but +# modified to conform to {the Shell & Utilities volume of the IEEE Std 1003.1-2008, 2016 +# Edition}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html] # # === Usage # @@ -62,12 +62,9 @@ # # === Contact # * Akinori MUSHA <knu@iDaemons.org> (current maintainer) -# -# === Resources -# -# 1: {IEEE Std 1003.1-2008, 2016 Edition, the Shell & Utilities volume}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html] module Shellwords + # The version number string. VERSION = "0.2.0" # Splits a string into an array of tokens in the same way the UNIX diff --git a/lib/tempfile.rb b/lib/tempfile.rb index 1d7b80a74d..2f8a39cfe5 100644 --- a/lib/tempfile.rb +++ b/lib/tempfile.rb @@ -8,18 +8,61 @@ require 'delegate' require 'tmpdir' -# A utility class for managing temporary files. When you create a Tempfile -# object, it will create a temporary file with a unique filename. A Tempfile -# objects behaves just like a File object, and you can perform all the usual -# file operations on it: reading data, writing data, changing its permissions, -# etc. So although this class does not explicitly document all instance methods -# supported by File, you can in fact call any File instance method on a -# Tempfile object. +# A utility class for managing temporary files. +# +# There are two kind of methods of creating a temporary file: +# +# - Tempfile.create (recommended) +# - Tempfile.new and Tempfile.open (mostly for backward compatibility, not recommended) +# +# Tempfile.create creates a usual \File object. +# The timing of file deletion is predictable. +# Also, it supports open-and-unlink technique which +# removes the temporary file immediately after creation. +# +# Tempfile.new and Tempfile.open creates a \Tempfile object. +# The created file is removed by the GC (finalizer). +# The timing of file deletion is not predictable. # # == Synopsis # # require 'tempfile' # +# # Tempfile.create with a block +# # The filename are choosen automatically. +# # (You can specify the prefix and suffix of the filename by an optional argument.) +# Tempfile.create {|f| +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# } # The file is removed at block exit. +# +# # Tempfile.create without a block +# # You need to unlink the file in non-block form. +# f = Tempfile.create +# f.puts "foo" +# f.close +# File.unlink(f.path) # You need to unlink the file. +# +# # Tempfile.create(anonymous: true) without a block +# f = Tempfile.create(anonymous: true) +# # The file is already removed because anonymous. +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# f.close +# +# # Tempfile.create(anonymous: true) with a block +# Tempfile.create(anonymous: true) {|f| +# # The file is already removed because anonymous. +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# } +# +# # Not recommended: Tempfile.new without a block # file = Tempfile.new('foo') # file.path # => A unique filename in the OS's temp directory, # # e.g.: "/tmp/foo.24722.0" @@ -30,7 +73,27 @@ require 'tmpdir' # file.close # file.unlink # deletes the temp file # -# == Good practices +# == About Tempfile.new and Tempfile.open +# +# This section does not apply to Tempfile.create because +# it returns a File object (not a Tempfile object). +# +# When you create a Tempfile object, +# it will create a temporary file with a unique filename. A Tempfile +# objects behaves just like a File object, and you can perform all the usual +# file operations on it: reading data, writing data, changing its permissions, +# etc. So although this class does not explicitly document all instance methods +# supported by File, you can in fact call any File instance method on a +# Tempfile object. +# +# A Tempfile object has a finalizer to remove the temporary file. +# This means that the temporary file is removed via GC. +# This can cause several problems: +# +# - Long GC intervals and conservative GC can accumulate temporary files that are not removed. +# - Temporary files are not removed if Ruby exits abnormally (such as SIGKILL, SEGV). +# +# There are legacy good practices for Tempfile.new and Tempfile.open as follows. # # === Explicit close # @@ -71,12 +134,17 @@ require 'tmpdir' # be able to read from or write to the Tempfile, and you do not need to # know the Tempfile's filename either. # +# Also, this guarantees the temporary file is removed even if Ruby exits abnormally. +# The OS reclaims the storage for the temporary file when the file is closed or +# the Ruby process exits (normally or abnormally). +# # For example, a practical use case for unlink-after-creation would be this: # you need a large byte buffer that's too large to comfortably fit in RAM, # e.g. when you're writing a web server and you want to buffer the client's # file upload data. # -# Please refer to #unlink for more information and a code example. +# `Tempfile.create(anonymous: true)` supports this behavior. +# It also works on Windows. # # == Minor notes # @@ -153,45 +221,47 @@ class Tempfile < DelegateClass(File) @unlinked = false @mode = mode|File::RDWR|File::CREAT|File::EXCL - @finalizer_obj = Object.new tmpfile = nil ::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts| opts[:perm] = 0600 tmpfile = File.open(tmpname, @mode, **opts) @opts = opts.freeze end - ObjectSpace.define_finalizer(@finalizer_obj, Remover.new(tmpfile.path)) - ObjectSpace.define_finalizer(self, Closer.new(tmpfile)) super(tmpfile) + + @finalizer_manager = FinalizerManager.new(__getobj__.path) + @finalizer_manager.register(self, __getobj__) end def initialize_dup(other) # :nodoc: initialize_copy_iv(other) super(other) - ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + @finalizer_manager.register(self, __getobj__) end def initialize_clone(other) # :nodoc: initialize_copy_iv(other) super(other) - ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + @finalizer_manager.register(self, __getobj__) end private def initialize_copy_iv(other) # :nodoc: @unlinked = other.unlinked @mode = other.mode @opts = other.opts - @finalizer_obj = other.finalizer_obj + @finalizer_manager = other.finalizer_manager end # Opens or reopens the file with mode "r+". def open _close - ObjectSpace.undefine_finalizer(self) + mode = @mode & ~(File::CREAT|File::EXCL) __setobj__(File.open(__getobj__.path, mode, **@opts)) - ObjectSpace.define_finalizer(self, Closer.new(__getobj__)) + + @finalizer_manager.register(self, __getobj__) + __getobj__ end @@ -259,7 +329,9 @@ class Tempfile < DelegateClass(File) # may not be able to unlink on Windows; just ignore return end - ObjectSpace.undefine_finalizer(@finalizer_obj) + + @finalizer_manager.unlinked = true + @unlinked = true end alias delete unlink @@ -293,35 +365,35 @@ class Tempfile < DelegateClass(File) protected - attr_reader :unlinked, :mode, :opts, :finalizer_obj + attr_reader :unlinked, :mode, :opts, :finalizer_manager - class Closer # :nodoc: - def initialize(tmpfile) - @tmpfile = tmpfile - end - - def call(*args) - @tmpfile.close - end - end + class FinalizerManager # :nodoc: + attr_accessor :unlinked - class Remover # :nodoc: def initialize(path) - @pid = Process.pid + @open_files = {} @path = path + @pid = Process.pid + @unlinked = false end - def call(*args) - return if @pid != Process.pid + def register(obj, file) + ObjectSpace.undefine_finalizer(obj) + ObjectSpace.define_finalizer(obj, self) + @open_files[obj.object_id] = file + end - $stderr.puts "removing #{@path}..." if $DEBUG + def call(object_id) + @open_files.delete(object_id).close - begin - File.unlink(@path) - rescue Errno::ENOENT + if @open_files.empty? && !@unlinked && Process.pid == @pid + $stderr.puts "removing #{@path}..." if $DEBUG + begin + File.unlink(@path) + rescue Errno::ENOENT + end + $stderr.puts "done" if $DEBUG end - - $stderr.puts "done" if $DEBUG end end @@ -392,8 +464,9 @@ end # see {File Permissions}[rdoc-ref:File@File+Permissions]. # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end). # -# With no block, the file is not removed automatically, -# and so should be explicitly removed. +# The temporary file removal depends on the keyword argument +anonymous+ and +# whether a block is given or not. +# See the description about the +anonymous+ keyword argument later. # # Example: # @@ -401,11 +474,36 @@ end # f.class # => File # f.path # => "/tmp/20220505-9795-17ky6f6" # f.stat.mode.to_s(8) # => "100600" +# f.close # File.exist?(f.path) # => true # File.unlink(f.path) # File.exist?(f.path) # => false # -# Argument +basename+, if given, may be one of: +# Tempfile.create {|f| +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# f.path # => "/tmp/20240524-380207-oma0ny" +# File.exist?(f.path) # => true +# } # The file is removed at block exit. +# +# f = Tempfile.create(anonymous: true) +# # The file is already removed because anonymous +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# f.close +# +# Tempfile.create(anonymous: true) {|f| +# # The file is already removed because anonymous +# f.path # => "/tmp/" (no filename since no file) +# f.puts "foo" +# f.rewind +# f.read # => "foo\n" +# } +# +# The argument +basename+, if given, may be one of the following: # # - A string: the generated filename begins with +basename+: # @@ -416,27 +514,57 @@ end # # Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg> # -# With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+: +# With arguments +basename+ and +tmpdir+, the file is created in the directory +tmpdir+: # # Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8> # -# Keyword arguments +mode+ and +options+ are passed directly to method +# Keyword arguments +mode+ and +options+ are passed directly to the method # {File.open}[rdoc-ref:File.open]: # -# - The value given with +mode+ must be an integer, +# - The value given for +mode+ must be an integer # and may be expressed as the logical OR of constants defined in # {File::Constants}[rdoc-ref:File::Constants]. # - For +options+, see {Open Options}[rdoc-ref:IO@Open+Options]. # -# With a block given, creates the file as above, passes it to the block, -# and returns the block's value; -# before the return, the file object is closed and the underlying file is removed: +# The keyword argument +anonymous+ specifies when the file is removed. +# +# - <tt>anonymous=false</tt> (default) without a block: the file is not removed. +# - <tt>anonymous=false</tt> (default) with a block: the file is removed after the block exits. +# - <tt>anonymous=true</tt> without a block: the file is removed before returning. +# - <tt>anonymous=true</tt> with a block: the file is removed before the block is called. +# +# In the first case (<tt>anonymous=false</tt> without a block), +# the file is not removed automatically. +# It should be explicitly closed. +# It can be used to rename to the desired filename. +# If the file is not needed, it should be explicitly removed. +# +# The File#path method of the created file object returns the temporary directory with a trailing slash +# when +anonymous+ is true. +# +# When a block is given, it creates the file as described above, passes it to the block, +# and returns the block's value. +# Before the returning, the file object is closed and the underlying file is removed: # # Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists" # +# Implementation note: +# +# The keyword argument +anonymous=true+ is implemented using FILE_SHARE_DELETE on Windows. +# O_TMPFILE is used on Linux. +# # Related: Tempfile.new. # -def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) +def Tempfile.create(basename="", tmpdir=nil, mode: 0, anonymous: false, **options, &block) + if anonymous + create_anonymous(basename, tmpdir, mode: mode, **options, &block) + else + create_with_filename(basename, tmpdir, mode: mode, **options, &block) + end +end + +class << Tempfile +private def create_with_filename(basename="", tmpdir=nil, mode: 0, **options) tmpfile = nil Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts| mode |= File::RDWR|File::CREAT|File::EXCL @@ -464,3 +592,51 @@ def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options) tmpfile end end + +File.open(IO::NULL) do |f| + File.new(f.fileno, autoclose: false, path: "").path +rescue IOError + module PathAttr # :nodoc: + attr_reader :path + + def self.set_path(file, path) + file.extend(self).instance_variable_set(:@path, path) + end + end +end + +private def create_anonymous(basename="", tmpdir=nil, mode: 0, **options, &block) + tmpfile = nil + tmpdir = Dir.tmpdir() if tmpdir.nil? + if defined?(File::TMPFILE) # O_TMPFILE since Linux 3.11 + begin + tmpfile = File.open(tmpdir, File::RDWR | File::TMPFILE, 0600) + rescue Errno::EISDIR, Errno::ENOENT, Errno::EOPNOTSUPP + # kernel or the filesystem does not support O_TMPFILE + # fallback to create-and-unlink + end + end + if tmpfile.nil? + mode |= File::SHARE_DELETE | File::BINARY # Windows needs them to unlink the opened file. + tmpfile = create_with_filename(basename, tmpdir, mode: mode, **options) + File.unlink(tmpfile.path) + tmppath = tmpfile.path + end + path = File.join(tmpdir, '') + unless tmppath == path + # clear path. + tmpfile.autoclose = false + tmpfile = File.new(tmpfile.fileno, mode: File::RDWR, path: path) + PathAttr.set_path(tmpfile, path) if defined?(PathAttr) + end + if block + begin + yield tmpfile + ensure + tmpfile.close + end + else + tmpfile + end +end +end diff --git a/lib/time.rb b/lib/time.rb index b565130b50..10f75b1dda 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -26,7 +26,7 @@ require 'date' class Time - VERSION = "0.3.0" + VERSION = "0.4.0" class << Time @@ -695,35 +695,36 @@ class Time getutc.strftime('%a, %d %b %Y %T GMT') end - # - # Returns a string which represents the time as a dateTime defined by XML - # Schema: - # - # CCYY-MM-DDThh:mm:ssTZD - # CCYY-MM-DDThh:mm:ss.sssTZD - # - # where TZD is Z or [+-]hh:mm. - # - # If self is a UTC time, Z is used as TZD. [+-]hh:mm is used otherwise. - # - # +fraction_digits+ specifies a number of digits to use for fractional - # seconds. Its default value is 0. - # - # require 'time' - # - # t = Time.now - # t.iso8601 # => "2011-10-05T22:26:12-04:00" - # - # You must require 'time' to use this method. - # - def xmlschema(fraction_digits=0) - fraction_digits = fraction_digits.to_i - s = strftime("%FT%T") - if fraction_digits > 0 - s << strftime(".%#{fraction_digits}N") + unless method_defined?(:xmlschema) + # + # Returns a string which represents the time as a dateTime defined by XML + # Schema: + # + # CCYY-MM-DDThh:mm:ssTZD + # CCYY-MM-DDThh:mm:ss.sssTZD + # + # where TZD is Z or [+-]hh:mm. + # + # If self is a UTC time, Z is used as TZD. [+-]hh:mm is used otherwise. + # + # +fraction_digits+ specifies a number of digits to use for fractional + # seconds. Its default value is 0. + # + # require 'time' + # + # t = Time.now + # t.iso8601 # => "2011-10-05T22:26:12-04:00" + # + # You must require 'time' to use this method. + # + def xmlschema(fraction_digits=0) + fraction_digits = fraction_digits.to_i + s = strftime("%FT%T") + if fraction_digits > 0 + s << strftime(".%#{fraction_digits}N") + end + s << (utc? ? 'Z' : strftime("%:z")) end - s << (utc? ? 'Z' : strftime("%:z")) end - alias iso8601 xmlschema + alias iso8601 xmlschema unless method_defined?(:iso8601) end - diff --git a/lib/timeout.rb b/lib/timeout.rb index c67a748856..6448f4f114 100644 --- a/lib/timeout.rb +++ b/lib/timeout.rb @@ -4,7 +4,7 @@ # == Synopsis # # require 'timeout' -# status = Timeout::timeout(5) { +# status = Timeout.timeout(5) { # # Something that should be interrupted if it takes more than 5 seconds... # } # @@ -13,10 +13,6 @@ # Timeout provides a way to auto-terminate a potentially long-running # operation if it hasn't finished in a fixed amount of time. # -# Previous versions didn't use a module for namespacing, however -# #timeout is provided for backwards compatibility. You -# should prefer Timeout.timeout instead. -# # == Copyright # # Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb index fe3e0e19d1..66ac7cfb32 100644 --- a/lib/tmpdir.rb +++ b/lib/tmpdir.rb @@ -148,7 +148,11 @@ class Dir # Generates and yields random names to create a temporary name def create(basename, tmpdir=nil, max_try: nil, **opts) origdir = tmpdir - tmpdir ||= tmpdir() + if tmpdir + raise ArgumentError, "empty parent path" if tmpdir.empty? + else + tmpdir = tmpdir() + end n = nil prefix, suffix = basename prefix = (String.try_convert(prefix) or diff --git a/lib/uri/common.rb b/lib/uri/common.rb index dce09fbc1e..904df10663 100644 --- a/lib/uri/common.rb +++ b/lib/uri/common.rb @@ -13,24 +13,46 @@ require_relative "rfc2396_parser" require_relative "rfc3986_parser" module URI - include RFC2396_REGEXP + RFC2396_PARSER = RFC2396_Parser.new + Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor) - REGEXP = RFC2396_REGEXP - Parser = RFC2396_Parser RFC3986_PARSER = RFC3986_Parser.new Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor) - # URI::Parser.new - DEFAULT_PARSER = Parser.new - DEFAULT_PARSER.pattern.each_pair do |sym, str| - unless REGEXP::PATTERN.const_defined?(sym) - REGEXP::PATTERN.const_set(sym, str) + DEFAULT_PARSER = RFC3986_PARSER + Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor) + + def self.parser=(parser = RFC3986_PARSER) + remove_const(:Parser) if defined?(::URI::Parser) + const_set("Parser", parser.class) + + remove_const(:REGEXP) if defined?(::URI::REGEXP) + remove_const(:PATTERN) if defined?(::URI::PATTERN) + if Parser == RFC2396_Parser + const_set("REGEXP", URI::RFC2396_REGEXP) + const_set("PATTERN", URI::RFC2396_REGEXP::PATTERN) + Parser.new.pattern.each_pair do |sym, str| + unless REGEXP::PATTERN.const_defined?(sym) + REGEXP::PATTERN.const_set(sym, str) + end + end + end + + Parser.new.regexp.each_pair do |sym, str| + remove_const(sym) if const_defined?(sym) + const_set(sym, str) end end - DEFAULT_PARSER.regexp.each_pair do |sym, str| - const_set(sym, str) + self.parser = RFC3986_PARSER + + def self.const_missing(const) + if value = RFC2396_PARSER.regexp[const] + warn "URI::#{const} is obsolete. Use RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE + value + else + super + end end - Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor) module Util # :nodoc: def make_components_hash(klass, array_hash) @@ -168,7 +190,7 @@ module URI # ["fragment", "top"]] # def self.split(uri) - RFC3986_PARSER.split(uri) + DEFAULT_PARSER.split(uri) end # Returns a new \URI object constructed from the given string +uri+: @@ -182,7 +204,7 @@ module URI # if it may contain invalid URI characters. # def self.parse(uri) - RFC3986_PARSER.parse(uri) + DEFAULT_PARSER.parse(uri) end # Merges the given URI strings +str+ @@ -209,7 +231,7 @@ module URI # # => #<URI::HTTP http://example.com/foo/bar> # def self.join(*str) - RFC3986_PARSER.join(*str) + DEFAULT_PARSER.join(*str) end # diff --git a/lib/uri/file.rb b/lib/uri/file.rb index 4ff0bc097e..940d361af8 100644 --- a/lib/uri/file.rb +++ b/lib/uri/file.rb @@ -70,17 +70,17 @@ module URI # raise InvalidURIError def check_userinfo(user) - raise URI::InvalidURIError, "can not set userinfo for file URI" + raise URI::InvalidURIError, "cannot set userinfo for file URI" end # raise InvalidURIError def check_user(user) - raise URI::InvalidURIError, "can not set user for file URI" + raise URI::InvalidURIError, "cannot set user for file URI" end # raise InvalidURIError def check_password(user) - raise URI::InvalidURIError, "can not set password for file URI" + raise URI::InvalidURIError, "cannot set password for file URI" end # do nothing diff --git a/lib/uri/generic.rb b/lib/uri/generic.rb index bdd366661e..d4bfa3b919 100644 --- a/lib/uri/generic.rb +++ b/lib/uri/generic.rb @@ -82,7 +82,7 @@ module URI if args.kind_of?(Array) return self.build(args.collect{|x| if x.is_a?(String) - DEFAULT_PARSER.escape(x) + URI::RFC2396_PARSER.escape(x) else x end @@ -91,7 +91,7 @@ module URI tmp = {} args.each do |key, value| tmp[key] = if value - DEFAULT_PARSER.escape(value) + URI::RFC2396_PARSER.escape(value) else value end @@ -393,7 +393,7 @@ module URI def check_user(v) if @opaque raise InvalidURIError, - "can not set user with opaque" + "cannot set user with opaque" end return v unless v @@ -417,7 +417,7 @@ module URI def check_password(v, user = @user) if @opaque raise InvalidURIError, - "can not set password with opaque" + "cannot set password with opaque" end return v unless v @@ -596,7 +596,7 @@ module URI if @opaque raise InvalidURIError, - "can not set host with registry or opaque" + "cannot set host with registry or opaque" elsif parser.regexp[:HOST] !~ v raise InvalidComponentError, "bad component(expected host component): #{v}" @@ -685,7 +685,7 @@ module URI if @opaque raise InvalidURIError, - "can not set port with registry or opaque" + "cannot set port with registry or opaque" elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v raise InvalidComponentError, "bad component(expected port component): #{v.inspect}" @@ -733,17 +733,17 @@ module URI end def check_registry(v) # :nodoc: - raise InvalidURIError, "can not set registry" + raise InvalidURIError, "cannot set registry" end private :check_registry def set_registry(v) #:nodoc: - raise InvalidURIError, "can not set registry" + raise InvalidURIError, "cannot set registry" end protected :set_registry def registry=(v) - raise InvalidURIError, "can not set registry" + raise InvalidURIError, "cannot set registry" end # @@ -866,7 +866,7 @@ module URI # hier_part = ( net_path | abs_path ) [ "?" query ] if @host || @port || @user || @path # userinfo = @user + ':' + @password raise InvalidURIError, - "can not set opaque with host, port, userinfo or path" + "cannot set opaque with host, port, userinfo or path" elsif v && parser.regexp[:OPAQUE] !~ v raise InvalidComponentError, "bad component(expected opaque component): #{v}" @@ -1235,7 +1235,7 @@ module URI return rel, rel end - # you can modify `rel', but can not `oth'. + # you can modify `rel', but cannot `oth'. return oth, rel end private :route_from0 @@ -1260,7 +1260,7 @@ module URI # #=> #<URI::Generic /main.rbx?page=1> # def route_from(oth) - # you can modify `rel', but can not `oth'. + # you can modify `rel', but cannot `oth'. begin oth, rel = route_from0(oth) rescue diff --git a/lib/uri/rfc2396_parser.rb b/lib/uri/rfc2396_parser.rb index 00c66cf042..a56ca34267 100644 --- a/lib/uri/rfc2396_parser.rb +++ b/lib/uri/rfc2396_parser.rb @@ -140,11 +140,11 @@ module URI if !scheme raise InvalidURIError, - "bad URI(absolute but no scheme): #{uri}" + "bad URI (absolute but no scheme): #{uri}" end if !opaque && (!path && (!host && !registry)) raise InvalidURIError, - "bad URI(absolute but no path): #{uri}" + "bad URI (absolute but no path): #{uri}" end when @regexp[:REL_URI] @@ -173,7 +173,7 @@ module URI # server = [ [ userinfo "@" ] hostport ] else - raise InvalidURIError, "bad URI(is not URI?): #{uri}" + raise InvalidURIError, "bad URI (is not URI?): #{uri}" end path = '' if !path && !opaque # (see RFC2396 Section 5.2) diff --git a/lib/uri/rfc3986_parser.rb b/lib/uri/rfc3986_parser.rb index 092a1ac89d..4000f1357f 100644 --- a/lib/uri/rfc3986_parser.rb +++ b/lib/uri/rfc3986_parser.rb @@ -78,7 +78,7 @@ module URI begin uri = uri.to_str rescue NoMethodError - raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}" + raise InvalidURIError, "bad URI (is not URI?): #{uri.inspect}" end uri.ascii_only? or raise InvalidURIError, "URI must be ascii only #{uri.dump}" @@ -127,7 +127,7 @@ module URI m["fragment"] ] else - raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}" + raise InvalidURIError, "bad URI (is not URI?): #{uri.inspect}" end end @@ -135,12 +135,35 @@ module URI URI.for(*self.split(uri), self) end - def join(*uris) # :nodoc: uris[0] = convert_to_uri(uris[0]) uris.inject :merge end + # Compatibility for RFC2396 parser + def extract(str, schemes = nil, &block) # :nodoc: + warn "URI::RFC3986_PARSER.extract is obsoleted. Use URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.extract(str, schemes, &block) + end + + # Compatibility for RFC2396 parser + def make_regexp(schemes = nil) # :nodoc: + warn "URI::RFC3986_PARSER.make_regexp is obsoleted. Use URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.make_regexp(schemes) + end + + # Compatibility for RFC2396 parser + def escape(str, unsafe = nil) # :nodoc: + warn "URI::RFC3986_PARSER.escape is obsoleted. Use URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE + unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str) + end + + # Compatibility for RFC2396 parser + def unescape(str, escaped = nil) # :nodoc: + warn "URI::RFC3986_PARSER.unescape is obsoleted. Use URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE + escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str) + end + @@to_s = Kernel.instance_method(:to_s) if @@to_s.respond_to?(:bind_call) def inspect diff --git a/lib/uri/version.rb b/lib/uri/version.rb index 2dafa57d59..bfe3f47670 100644 --- a/lib/uri/version.rb +++ b/lib/uri/version.rb @@ -1,6 +1,6 @@ module URI # :stopdoc: - VERSION_CODE = '001300'.freeze + VERSION_CODE = '001301'.freeze VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze # :startdoc: end |