diff options
Diffstat (limited to 'lib')
108 files changed, 2150 insertions, 1104 deletions
diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb index a340463c98..c933ad0471 100644 --- a/lib/bundled_gems.rb +++ b/lib/bundled_gems.rb @@ -28,6 +28,7 @@ module Gem::BUNDLED_GEMS "syslog" => "3.4.0", "ostruct" => "3.5.0", "pstore" => "3.5.0", + "rdoc" => "3.5.0", }.freeze EXACT = { @@ -45,6 +46,7 @@ module Gem::BUNDLED_GEMS "syslog" => true, "ostruct" => true, "pstore" => true, + "rdoc" => true, }.freeze PREFIXED = { @@ -101,8 +103,11 @@ module Gem::BUNDLED_GEMS def self.warning?(name, specs: nil) # name can be a feature name or a file path with String or Pathname feature = File.path(name) - # bootsnap expand `require "csv"` to `require "#{LIBDIR}/csv.rb"` - name = feature.delete_prefix(LIBDIR).chomp(".rb").tr("/", "-") + # bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`, + # and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`. + name = feature.delete_prefix(ARCHDIR) + name.delete_prefix!(LIBDIR) + name.tr!("/", "-") name.sub!(LIBEXT, "") return if specs.include?(name) _t, path = $:.resolve_feature_path(feature) diff --git a/lib/bundler.rb b/lib/bundler.rb index 59a1107bb7..5033109db6 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -40,6 +40,7 @@ module Bundler SUDO_MUTEX = Thread::Mutex.new 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 :Definition, File.expand_path("bundler/definition", __dir__) autoload :Dependency, File.expand_path("bundler/dependency", __dir__) @@ -165,6 +166,25 @@ module Bundler end end + # Automatically install dependencies if Bundler.settings[:auto_install] exists. + # This is set through config cmd `bundle config set --global auto_install 1`. + # + # Note that this method `nil`s out the global Definition object, so it + # should be called first, before you instantiate anything like an + # `Installer` that'll keep a reference to the old one instead. + def auto_install + return unless settings[:auto_install] + + begin + definition.specs + rescue GemNotFound, GitError + ui.info "Automatically installing missing gems." + reset! + CLI::Install.new({}).run + reset! + end + end + # Setups Bundler environment (see Bundler.setup) if it is not already set, # and loads all gems from groups specified. Unlike ::setup, can be called # multiple times with different groups (if they were allowed by setup). @@ -184,6 +204,7 @@ module Bundler # Bundler.require(:test) # requires second_gem # def require(*groups) + load_plugins setup(*groups).require(*groups) end @@ -560,6 +581,23 @@ module Bundler @feature_flag ||= FeatureFlag.new(VERSION) end + def load_plugins(definition = Bundler.definition) + return if defined?(@load_plugins_ran) + + Bundler.rubygems.load_plugins + + requested_path_gems = definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } + path_plugin_files = requested_path_gems.map do |spec| + Bundler.rubygems.spec_matches_for_glob(spec, "rubygems_plugin#{Bundler.rubygems.suffix_pattern}") + rescue TypeError + error_message = "#{spec.name} #{spec.version} has an invalid gemspec" + raise Gem::InvalidSpecificationException, error_message + end.flatten + Bundler.rubygems.load_plugin_files(path_plugin_files) + Bundler.rubygems.load_env_plugins + @load_plugins_ran = true + end + def reset! reset_paths! Plugin.reset! diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 8db725bbde..40f19c7fed 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -5,6 +5,7 @@ require_relative "vendored_thor" module Bundler class CLI < Thor require_relative "cli/common" + require_relative "cli/install" package_name "Bundler" @@ -69,7 +70,7 @@ module Bundler Bundler.settings.set_command_option_if_given :retry, options[:retry] current_cmd = args.last[:current_command].name - auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) + Bundler.auto_install if AUTO_INSTALL_CMDS.include?(current_cmd) rescue UnknownArgumentError => e raise InvalidOption, e.message ensure @@ -114,6 +115,8 @@ module Bundler class_option "verbose", type: :boolean, desc: "Enable verbose output mode", aliases: "-V" def help(cli = nil) + cli = self.class.all_aliases[cli] if self.class.all_aliases[cli] + case cli when "gemfile" then command = "gemfile" when nil then command = "bundle" @@ -683,7 +686,6 @@ module Bundler exec_used = args.index {|a| exec_commands.include? a } command = args.find {|a| bundler_commands.include? a } - command = all_aliases[command] if all_aliases[command] if exec_used && help_used if exec_used + help_used == 1 @@ -736,26 +738,6 @@ module Bundler private - # Automatically invoke `bundle install` and resume if - # Bundler.settings[:auto_install] exists. This is set through config cmd - # `bundle config set --global auto_install 1`. - # - # Note that this method `nil`s out the global Definition object, so it - # should be called first, before you instantiate anything like an - # `Installer` that'll keep a reference to the old one instead. - def auto_install - return unless Bundler.settings[:auto_install] - - begin - Bundler.definition.specs - rescue GemNotFound, GitError - Bundler.ui.info "Automatically installing missing gems." - Bundler.reset! - invoke :install, [] - Bundler.reset! - end - end - def current_command _, _, config = @_initializer config[:current_command] diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb index de9698b577..bcbd228b18 100644 --- a/lib/bundler/constants.rb +++ b/lib/bundler/constants.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "rbconfig" + module Bundler WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/ FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd") diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index c8faf77b3b..22070b6b17 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -92,11 +92,12 @@ module Bundler @platforms = @locked_platforms.dup @locked_bundler_version = @locked_gems.bundler_version @locked_ruby_version = @locked_gems.ruby_version + @originally_locked_deps = @locked_gems.dependencies @originally_locked_specs = SpecSet.new(@locked_gems.specs) @locked_checksums = @locked_gems.checksums if unlock != true - @locked_deps = @locked_gems.dependencies + @locked_deps = @originally_locked_deps @locked_specs = @originally_locked_specs @locked_sources = @locked_gems.sources else @@ -111,6 +112,7 @@ module Bundler @locked_gems = nil @locked_deps = {} @locked_specs = SpecSet.new([]) + @originally_locked_deps = {} @originally_locked_specs = @locked_specs @locked_sources = [] @locked_platforms = [] @@ -130,7 +132,7 @@ module Bundler @sources.merged_gem_lockfile_sections!(locked_gem_sources.first) end - @unlock[:sources] ||= [] + @sources_to_unlock = @unlock.delete(:sources) || [] @unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object @ruby_version.diff(locked_ruby_version_object) end @@ -142,11 +144,13 @@ module Bundler @path_changes = converge_paths @source_changes = converge_sources + @explicit_unlocks = @unlock.delete(:gems) || [] + if @unlock[:conservative] - @unlock[:gems] ||= @dependencies.map(&:name) + @gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name) else - eager_unlock = (@unlock[:gems] || []).map {|name| Dependency.new(name, ">= 0") } - @unlock[:gems] = @locked_specs.for(eager_unlock, false, platforms).map(&:name).uniq + eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") } + @gems_to_unlock = @locked_specs.for(eager_unlock, false, platforms).map(&:name).uniq end @dependency_changes = converge_dependencies @@ -225,7 +229,6 @@ module Bundler @resolver = nil @resolution_packages = nil @specs = nil - @gem_version_promoter = nil Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})" true @@ -566,8 +569,10 @@ module Bundler @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: @unlock[:gems], prerelease: gem_version_promoter.pre?) - additional_base_requirements_for_resolve(packages, last_resolve) + 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?) + packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve) + packages = additional_base_requirements_to_force_updates(packages) + packages end end @@ -671,14 +676,18 @@ module Bundler def change_reason if unlocking? - unlock_reason = @unlock.reject {|_k, v| Array(v).empty? }.map do |k, v| - if v == true - k.to_s - else - v = Array(v) - "#{k}: (#{v.join(", ")})" - end - end.join(", ") + unlock_targets = if @gems_to_unlock.any? + ["gems", @gems_to_unlock] + elsif @sources_to_unlock.any? + ["sources", @sources_to_unlock] + end + + unlock_reason = if unlock_targets + "#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})" + else + @unlock[:ruby] ? "ruby" : "" + end + return "bundler is unlocking #{unlock_reason}" end [ @@ -733,7 +742,7 @@ module Bundler spec = @dependencies.find {|s| s.name == k } source = spec&.source if source&.respond_to?(:local_override!) - source.unlock! if @unlock[:gems].include?(spec.name) + source.unlock! if @gems_to_unlock.include?(spec.name) locals << [source, source.local_override!(v)] end end @@ -741,7 +750,7 @@ module Bundler sources_with_changes = locals.select do |source, changed| changed || specs_changed?(source) end.map(&:first) - !sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty? + !sources_with_changes.each {|source| @sources_to_unlock << source.name }.empty? end def check_lockfile @@ -818,7 +827,7 @@ module Bundler # gem), unlock it. For git sources, this means to unlock the revision, which # will cause the `ref` used to be the most recent for the branch (or master) if # an explicit `ref` is not used. - if source.respond_to?(:unlock!) && @unlock[:sources].include?(source.name) + if source.respond_to?(:unlock!) && @sources_to_unlock.include?(source.name) source.unlock! changes = true end @@ -835,9 +844,7 @@ module Bundler dep.source = sources.get(dep.source) end - next if unlocking? - - unless locked_dep = @locked_deps[dep.name] + unless locked_dep = @originally_locked_deps[dep.name] changes = true next end @@ -864,7 +871,7 @@ module Bundler def converge_locked_specs converged = converge_specs(@locked_specs) - resolve = SpecSet.new(converged.reject {|s| @unlock[:gems].include?(s.name) }) + resolve = SpecSet.new(converged.reject {|s| @gems_to_unlock.include?(s.name) }) diff = nil @@ -897,7 +904,7 @@ module Bundler @specs_that_changed_sources << s if gemfile_source != lockfile_source deps << dep if !dep.source || lockfile_source.include?(dep.source) - @unlock[:gems] << name if lockfile_source.include?(dep.source) && lockfile_source != gemfile_source + @gems_to_unlock << name if lockfile_source.include?(dep.source) && lockfile_source != gemfile_source # Replace the locked dependency's source with the equivalent source from the Gemfile s.source = gemfile_source @@ -906,7 +913,7 @@ module Bundler s.source = default_source unless sources.get(lockfile_source) end - next if @unlock[:sources].include?(s.source.name) + next if @sources_to_unlock.include?(s.source.name) # Path sources have special logic if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec) @@ -928,12 +935,12 @@ module Bundler else # If the spec is no longer in the path source, unlock it. This # commonly happens if the version changed in the gemspec - @unlock[:gems] << name + @gems_to_unlock << name end end if dep.nil? && requested_dependencies.find {|d| name == d.name } - @unlock[:gems] << s.name + @gems_to_unlock << s.name else converged << s end @@ -1010,7 +1017,7 @@ module Bundler current == proposed end - def additional_base_requirements_for_resolve(resolution_packages, last_resolve) + def additional_base_requirements_to_prevent_downgrades(resolution_packages, last_resolve) return resolution_packages unless @locked_gems && !sources.expired_sources?(@locked_gems.sources) converge_specs(@originally_locked_specs - last_resolve).each do |locked_spec| next if locked_spec.source.is_a?(Source::Path) @@ -1019,6 +1026,28 @@ module Bundler resolution_packages end + def additional_base_requirements_to_force_updates(resolution_packages) + return resolution_packages if @explicit_unlocks.empty? + full_update = dup_for_full_unlock.resolve + @explicit_unlocks.each do |name| + version = full_update[name].first&.version + resolution_packages.base_requirements[name] = Gem::Requirement.new("= #{version}") if version + end + resolution_packages + end + + 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.gem_version_promoter.tap do |gvp| + gvp.level = gem_version_promoter.level + gvp.strict = gem_version_promoter.strict + gvp.pre = gem_version_promoter.pre + end + unlocked_definition + end + def remove_invalid_platforms!(dependencies) return if Bundler.frozen_bundle? diff --git a/lib/bundler/environment_preserver.rb b/lib/bundler/environment_preserver.rb index c4c1b53fa4..444ab6fd37 100644 --- a/lib/bundler/environment_preserver.rb +++ b/lib/bundler/environment_preserver.rb @@ -19,14 +19,7 @@ module Bundler BUNDLER_PREFIX = "BUNDLER_ORIG_" def self.from_env - new(env_to_hash(ENV), BUNDLER_KEYS) - end - - def self.env_to_hash(env) - to_hash = env.to_hash - return to_hash unless Gem.win_platform? - - to_hash.each_with_object({}) {|(k,v), a| a[k.upcase] = v } + new(ENV.to_hash, BUNDLER_KEYS) end # @param env [Hash] @@ -39,18 +32,7 @@ module Bundler # Replaces `ENV` with the bundler environment variables backed up def replace_with_backup - unless Gem.win_platform? - ENV.replace(backup) - return - end - - # Fallback logic for Windows below to workaround - # https://bugs.ruby-lang.org/issues/16798. Can be dropped once all - # supported rubies include the fix for that. - - ENV.clear - - backup.each {|k, v| ENV[k] = v } + ENV.replace(backup) end # @return [Hash] diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index b6a11cc721..c29b1bfed8 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -230,4 +230,18 @@ module Bundler 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 end diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb index 018324f840..72e5602cc3 100644 --- a/lib/bundler/installer.rb +++ b/lib/bundler/installer.rb @@ -81,7 +81,7 @@ module Bundler if resolve_if_needed(options) ensure_specs_are_compatible! - load_plugins + Bundler.load_plugins(@definition) options.delete(:jobs) else options[:jobs] = 1 # to avoid the overhead of Bundler::Worker @@ -213,20 +213,6 @@ module Bundler Bundler.settings.processor_count end - def load_plugins - Gem.load_plugins - - requested_path_gems = @definition.requested_specs.select {|s| s.source.is_a?(Source::Path) } - path_plugin_files = requested_path_gems.map do |spec| - Bundler.rubygems.spec_matches_for_glob(spec, "rubygems_plugin#{Bundler.rubygems.suffix_pattern}") - rescue TypeError - error_message = "#{spec.name} #{spec.version} has an invalid gemspec" - raise Gem::InvalidSpecificationException, error_message - end.flatten - Gem.load_plugin_files(path_plugin_files) - Gem.load_env_plugins - end - def ensure_specs_are_compatible! @definition.specs.each do |spec| unless spec.matches_current_ruby? diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 0b09480f88..56a3b6f85c 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,6 +1,6 @@ .\" generated with nRonn/v0.11.1 .\" https://github.com/n-ronn/nronn/tree/0.11.1 -.TH "BUNDLE\-ADD" "1" "April 2024" "" +.TH "BUNDLE\-ADD" "1" "May 2024" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index fdb86bfd29..4ec301951f 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" "April 2024" "" +.TH "BUNDLE\-BINSTUBS" "1" "May 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 c5899ace1a..e2da1269e6 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" "April 2024" "" +.TH "BUNDLE\-CACHE" "1" "May 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 08d41b7881..dee1af1326 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" "April 2024" "" +.TH "BUNDLE\-CHECK" "1" "May 2024" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" @@ -9,6 +9,8 @@ \fBcheck\fR searches the local machine for each of the gems requested in the Gemfile\. If all gems are found, Bundler prints a success message and exits with a status of 0\. .P If not, the first missing gem is listed and Bundler exits status 1\. +.P +If the lockfile needs to be updated then it will be resolved using the gems installed on the local machine, if they satisfy the requirements\. .SH "OPTIONS" .TP \fB\-\-dry\-run\fR diff --git a/lib/bundler/man/bundle-check.1.ronn b/lib/bundler/man/bundle-check.1.ronn index f2846b8ff2..eb3ff1daf9 100644 --- a/lib/bundler/man/bundle-check.1.ronn +++ b/lib/bundler/man/bundle-check.1.ronn @@ -15,6 +15,9 @@ a status of 0. If not, the first missing gem is listed and Bundler exits status 1. +If the lockfile needs to be updated then it will be resolved using the gems +installed on the local machine, if they satisfy the requirements. + ## OPTIONS * `--dry-run`: diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index ca2fb65f59..7c7f9b5c77 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" "April 2024" "" +.TH "BUNDLE\-CLEAN" "1" "May 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 a78c6b0a18..a207dbbcaa 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" "April 2024" "" +.TH "BUNDLE\-CONFIG" "1" "May 2024" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index 86be5e4185..dca18ec43d 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" "April 2024" "" +.TH "BUNDLE\-CONSOLE" "1" "May 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 94cbff4cbd..6489cc07f7 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" "April 2024" "" +.TH "BUNDLE\-DOCTOR" "1" "May 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 7aa49e0096..1548d29670 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" "April 2024" "" +.TH "BUNDLE\-EXEC" "1" "May 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 89eb9f1980..5df7b0ef2f 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" "April 2024" "" +.TH "BUNDLE\-GEM" "1" "May 2024" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 index 441ec12735..a3e7c7770d 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" "April 2024" "" +.TH "BUNDLE\-HELP" "1" "May 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 287965c06a..a3d7ff0988 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" "April 2024" "" +.TH "BUNDLE\-INFO" "1" "May 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 d13f8ddc3b..a0edaaa18f 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" "April 2024" "" +.TH "BUNDLE\-INIT" "1" "May 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 086fc88156..7a1038206e 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" "April 2024" "" +.TH "BUNDLE\-INJECT" "1" "May 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 762c99e7c0..cc46a03b7f 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" "April 2024" "" +.TH "BUNDLE\-INSTALL" "1" "May 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 93db700091..21723608cc 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" "April 2024" "" +.TH "BUNDLE\-LIST" "1" "May 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 c0190f91de..8b81b7732f 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" "April 2024" "" +.TH "BUNDLE\-LOCK" "1" "May 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 38bacb67dc..41a8804a09 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" "April 2024" "" +.TH "BUNDLE\-OPEN" "1" "May 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 cee9d63155..838fce45cd 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" "April 2024" "" +.TH "BUNDLE\-OUTDATED" "1" "May 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 069966f1b2..0dbce7a7a4 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" "April 2024" "" +.TH "BUNDLE\-PLATFORM" "1" "May 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 f4e043c363..1290abb3ba 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" "April 2024" "" +.TH "BUNDLE\-PLUGIN" "1" "May 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 76a0479bc6..bf4a7cd323 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" "April 2024" "" +.TH "BUNDLE\-PRISTINE" "1" "May 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 90a664e331..c3c96b416d 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" "April 2024" "" +.TH "BUNDLE\-REMOVE" "1" "May 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 f9f1fd1fa3..c054875efd 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" "April 2024" "" +.TH "BUNDLE\-SHOW" "1" "May 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 340ebac687..acd80ec75c 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" "April 2024" "" +.TH "BUNDLE\-UPDATE" "1" "May 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 00ff0153cb..44eaf92224 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" "April 2024" "" +.TH "BUNDLE\-VERSION" "1" "May 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 eba3b7df25..77b902214c 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" "April 2024" "" +.TH "BUNDLE\-VIZ" "1" "May 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 4bd2bcaf67..199d1ce9fd 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" "April 2024" "" +.TH "BUNDLE" "1" "May 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 c9478a953e..fa9e31f615 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" "April 2024" "" +.TH "GEMFILE" "5" "May 2024" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" diff --git a/lib/bundler/plugin/events.rb b/lib/bundler/plugin/events.rb index bc037d1af5..29c05098ae 100644 --- a/lib/bundler/plugin/events.rb +++ b/lib/bundler/plugin/events.rb @@ -56,6 +56,30 @@ module Bundler # Includes an Array of Bundler::Dependency objects # GEM_AFTER_INSTALL_ALL = "after-install-all" define :GEM_AFTER_INSTALL_ALL, "after-install-all" + + # @!parse + # A hook called before each individual gem is required + # Includes a Bundler::Dependency. + # GEM_BEFORE_REQUIRE = "before-require" + define :GEM_BEFORE_REQUIRE, "before-require" + + # @!parse + # A hook called after each individual gem is required + # Includes a Bundler::Dependency. + # GEM_AFTER_REQUIRE = "after-require" + define :GEM_AFTER_REQUIRE, "after-require" + + # @!parse + # A hook called before any gems require + # Includes an Array of Bundler::Dependency objects. + # GEM_BEFORE_REQUIRE_ALL = "before-require-all" + define :GEM_BEFORE_REQUIRE_ALL, "before-require-all" + + # @!parse + # A hook called after all gems required + # Includes an Array of Bundler::Dependency objects. + # GEM_AFTER_REQUIRE_ALL = "after-require-all" + define :GEM_AFTER_REQUIRE_ALL, "after-require-all" end end end diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index e0582beba2..14b8703213 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,12 @@ unless Gem.ruby_version.to_s == RUBY_VERSION || RUBY_PATCHLEVEL == -1 end module Gem + require "rubygems/specification" + class Specification + require_relative "match_metadata" + require_relative "match_platform" + include ::Bundler::MatchMetadata include ::Bundler::MatchPlatform @@ -48,7 +45,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 @@ -146,7 +143,23 @@ module Gem end end + module BetterPermissionError + def data + 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_accessor :source, :groups diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb index da555681f9..494030eab2 100644 --- a/lib/bundler/rubygems_integration.rb +++ b/lib/bundler/rubygems_integration.rb @@ -156,6 +156,18 @@ module Bundler loaded_gem_paths.flatten end + def load_plugins + Gem.load_plugins + end + + def load_plugin_files(plugin_files) + Gem.load_plugin_files(plugin_files) + end + + def load_env_plugins + Gem.load_env_plugins + end + def ui=(obj) Gem::DefaultUserInteraction.ui = obj end diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index ec772cfe7b..54aa30ce0b 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -41,12 +41,17 @@ module Bundler groups.map!(&:to_sym) groups = [:default] if groups.empty? - @definition.dependencies.each do |dep| - # Skip the dependency if it is not in any of the requested groups, or - # not for the current platform, or doesn't match the gem constraints. - next unless (dep.groups & groups).any? && dep.should_include? + dependencies = @definition.dependencies.select do |dep| + # Select the dependency if it is in any of the requested groups, and + # for the current platform, and matches the gem constraints. + (dep.groups & groups).any? && dep.should_include? + end + + Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE_ALL, dependencies) + dependencies.each do |dep| required_file = nil + Plugin.hook(Plugin::Events::GEM_BEFORE_REQUIRE, dep) begin # Loop through all the specified autorequires for the @@ -76,7 +81,13 @@ module Bundler end end end + + Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE, dep) end + + Plugin.hook(Plugin::Events::GEM_AFTER_REQUIRE_ALL, dependencies) + + dependencies end def self.definition_method(meth) diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb index 7131d15055..6010d66742 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" + # try to auto_install first before we get to the `Bundler.ui.silence`, so user knows what is happening + Bundler.auto_install + if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"] begin Bundler.ui.silence { Bundler.setup } diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 78760e6fa4..28f0cdff19 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true -require "pathname" -require "rbconfig" - require_relative "version" -require_relative "constants" require_relative "rubygems_integration" require_relative "current_ruby" 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 + autoload :Pathname, "pathname" + def root gemfile = find_gemfile raise GemfileNotFound, "Could not locate Gemfile" unless gemfile 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/did_you_mean/did_you_mean.gemspec b/lib/did_you_mean/did_you_mean.gemspec index 8fe5723129..be4ac76b4b 100644 --- a/lib/did_you_mean/did_you_mean.gemspec +++ b/lib/did_you_mean/did_you_mean.gemspec @@ -22,6 +22,4 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.required_ruby_version = '>= 2.5.0' - - spec.add_development_dependency "rake" end diff --git a/lib/irb.rb b/lib/irb.rb index ab50c797c7..b3435c257e 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -311,7 +311,9 @@ require_relative "irb/pager" # ### Input Method # # The IRB input method determines how command input is to be read; by default, -# the input method for a session is IRB::RelineInputMethod. +# the input method for a session is IRB::RelineInputMethod. Unless the +# value of the TERM environment variable is 'dumb', in which case the +# most simplistic input method is used. # # You can set the input method by: # @@ -329,7 +331,8 @@ require_relative "irb/pager" # IRB::ReadlineInputMethod. # * `--nosingleline` or `--multiline` sets the input method to # IRB::RelineInputMethod. -# +# * `--nosingleline` together with `--nomultiline` sets the +# input to IRB::StdioInputMethod. # # # Method `conf.use_multiline?` and its synonym `conf.use_reline` return: @@ -656,8 +659,10 @@ require_relative "irb/pager" # * `%m`: the value of `self.to_s`. # * `%M`: the value of `self.inspect`. # * `%l`: an indication of the type of string; one of `"`, `'`, `/`, `]`. -# * `*NN*i`: Indentation level. -# * `*NN*n`: Line number. +# * `%NNi`: Indentation level. NN is a 2-digit number that specifies the number +# of digits of the indentation level (03 will result in 001). +# * `%NNn`: Line number. NN is a 2-digit number that specifies the number +# of digits of the line number (03 will result in 001). # * `%%`: Literal `%`. # # @@ -926,8 +931,11 @@ module IRB # The lexer used by this irb session attr_accessor :scanner + attr_reader :from_binding + # Creates a new irb session - def initialize(workspace = nil, input_method = nil) + def initialize(workspace = nil, input_method = nil, from_binding: false) + @from_binding = from_binding @context = Context.new(self, workspace, input_method) @context.workspace.load_helper_methods_to_main @signal_status = :IN_IRB @@ -960,20 +968,26 @@ module IRB # # Irb#eval_input will simply return the input, and we need to pass it to the # debugger. - input = if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving? - # Previous IRB session's history has been saved when `Irb#run` is exited We need - # to make sure the saved history is not saved again by resetting the counter - context.io.reset_history_counter + input = nil + forced_exit = catch(:IRB_EXIT) do + if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving? + # Previous IRB session's history has been saved when `Irb#run` is exited We need + # to make sure the saved history is not saved again by resetting the counter + context.io.reset_history_counter - begin - eval_input - ensure - context.io.save_history + begin + input = eval_input + ensure + context.io.save_history + end + else + input = eval_input end - else - eval_input + false end + Kernel.exit if forced_exit + if input&.include?("\n") @line_no += input.count("\n") - 1 end @@ -984,6 +998,7 @@ module IRB def run(conf = IRB.conf) in_nested_session = !!conf[:MAIN_CONTEXT] conf[:IRB_RC].call(context) if conf[:IRB_RC] + prev_context = conf[:MAIN_CONTEXT] conf[:MAIN_CONTEXT] = context save_history = !in_nested_session && conf[:SAVE_HISTORY] && context.io.support_history_saving? @@ -1006,6 +1021,9 @@ module IRB eval_input end ensure + # Do not restore to nil. It will cause IRB crash when used with threads. + IRB.conf[:MAIN_CONTEXT] = prev_context if prev_context + RubyVM.keep_script_lines = keep_script_lines_backup if defined?(RubyVM.keep_script_lines) trap("SIGINT", prev_trap) conf[:AT_EXIT].each{|hook| hook.call} @@ -1112,7 +1130,7 @@ module IRB code.force_encoding(@context.io.encoding) if (command, arg = parse_command(code)) - command_class = ExtendCommandBundle.load_command(command) + command_class = Command.load_command(command) Statement::Command.new(code, command_class, arg) else is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables) @@ -1134,7 +1152,7 @@ module IRB # 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 ExtendCommandBundle.execute_as_command?(command, public_method: public_method, private_method: private_method) + if Command.execute_as_command?(command, public_method: public_method, private_method: private_method) [command, arg] end end @@ -1230,27 +1248,33 @@ module IRB irb_bug = true else irb_bug = false - # This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace - # In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message - # And we clone the exception object in order to avoid mutating the original exception - # TODO: introduce better API to expose exception backtrace externally - backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact + # To support backtrace filtering while utilizing Exception#full_message, we need to clone + # the exception to avoid modifying the original exception's backtrace. exc = exc.clone - exc.set_backtrace(backtrace) - end + filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact + backtrace_filter = IRB.conf[:BACKTRACE_FILTER] - if RUBY_VERSION < '3.0.0' - if STDOUT.tty? - message = exc.full_message(order: :bottom) - order = :bottom - else - message = exc.full_message(order: :top) - order = :top + if backtrace_filter + if backtrace_filter.respond_to?(:call) + filtered_backtrace = backtrace_filter.call(filtered_backtrace) + else + warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method" + end end - else # '3.0.0' <= RUBY_VERSION - message = exc.full_message(order: :top) - order = :top + + exc.set_backtrace(filtered_backtrace) end + + highlight = Color.colorable? + + order = + if RUBY_VERSION < '3.0.0' + STDOUT.tty? ? :bottom : :top + else # '3.0.0' <= RUBY_VERSION + :top + end + + message = exc.full_message(order: order, highlight: highlight) message = convert_invalid_byte_sequence(message, exc.message.encoding) message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s) message = message.gsub(/((?:^\t.+$\n)+)/) { |m| @@ -1455,7 +1479,7 @@ module IRB end def format_prompt(format, ltype, indent, line_no) # :nodoc: - format.gsub(/%([0-9]+)?([a-zA-Z])/) do + format.gsub(/%([0-9]+)?([a-zA-Z%])/) do case $2 when "N" @context.irb_name @@ -1488,7 +1512,7 @@ module IRB line_no.to_s end when "%" - "%" + "%" unless $1 end end end @@ -1575,7 +1599,7 @@ class Binding else # If we're not in a debugger session, create a new IRB instance with the current # workspace - binding_irb = IRB::Irb.new(workspace) + binding_irb = IRB::Irb.new(workspace, from_binding: true) binding_irb.context.irb_path = irb_path binding_irb.run(IRB.conf) binding_irb.debug_break diff --git a/lib/irb/color.rb b/lib/irb/color.rb index ad8670160c..fca942b28b 100644 --- a/lib/irb/color.rb +++ b/lib/irb/color.rb @@ -79,12 +79,12 @@ module IRB # :nodoc: class << self def colorable? - supported = $stdout.tty? && (/mswin|mingw/ =~ RUBY_PLATFORM || (ENV.key?('TERM') && ENV['TERM'] != 'dumb')) + supported = $stdout.tty? && (/mswin|mingw/.match?(RUBY_PLATFORM) || (ENV.key?('TERM') && ENV['TERM'] != 'dumb')) # because ruby/debug also uses irb's color module selectively, # irb won't be activated in that case. if IRB.respond_to?(:conf) - supported && IRB.conf.fetch(:USE_COLORIZE, true) + supported && !!IRB.conf.fetch(:USE_COLORIZE, true) else supported end diff --git a/lib/irb/command.rb b/lib/irb/command.rb index 19fde56356..68a4b52727 100644 --- a/lib/irb/command.rb +++ b/lib/irb/command.rb @@ -16,13 +16,7 @@ module IRB # :nodoc: # Registers a command with the given name. # Aliasing is intentionally not supported at the moment. def register(name, command_class) - @commands[name] = [command_class, []] - end - - # This API is for IRB's internal use only and may change at any time. - # Please do NOT use it. - def _register_with_aliases(name, command_class, *aliases) - @commands[name] = [command_class, aliases] + @commands[name.to_sym] = [command_class, []] end end end diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb index b078b48237..1d406630a2 100644 --- a/lib/irb/command/base.rb +++ b/lib/irb/command/base.rb @@ -18,12 +18,12 @@ module IRB class << self def category(category = nil) @category = category if category - @category + @category || "No category" end def description(description = nil) @description = description if description - @description + @description || "No description provided." end def help_message(help_message = nil) diff --git a/lib/irb/command/chws.rb b/lib/irb/command/chws.rb index e0a406885f..ef456d0961 100644 --- a/lib/irb/command/chws.rb +++ b/lib/irb/command/chws.rb @@ -15,7 +15,7 @@ module IRB description "Show the current workspace." def execute(_arg) - irb_context.main + puts "Current workspace: #{irb_context.main}" end end @@ -30,7 +30,8 @@ module IRB obj = eval(arg, irb_context.workspace.binding) irb_context.change_workspace(obj) end - irb_context.main + + puts "Current workspace: #{irb_context.main}" end end end diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb index f9aca0a672..8a091a49ed 100644 --- a/lib/irb/command/debug.rb +++ b/lib/irb/command/debug.rb @@ -8,11 +8,6 @@ module IRB category "Debugging" description "Start the debugger of debug.gem." - BINDING_IRB_FRAME_REGEXPS = [ - '<internal:prelude>', - binding.method(:irb).source_location.first, - ].map { |file| /\A#{Regexp.escape(file)}:\d+:in (`|'Binding#)irb'\z/ } - def execute(_arg) execute_debug_command end @@ -36,7 +31,7 @@ module IRB # 3. Insert a debug breakpoint at `Irb#debug_break` with the intended command. # 4. Exit the current Irb#run call via `throw :IRB_EXIT`. # 5. `Irb#debug_break` will be called and trigger the breakpoint, which will run the intended command. - unless binding_irb? + unless irb_context.from_binding? puts "Debugging commands are only available when IRB is started with binding.irb" return end @@ -60,16 +55,6 @@ module IRB throw :IRB_EXIT end end - - private - - def binding_irb? - caller.any? do |frame| - BINDING_IRB_FRAME_REGEXPS.any? do |regexp| - frame.match?(regexp) - end - end - end end class DebugCommand < Debug diff --git a/lib/irb/command/exit.rb b/lib/irb/command/exit.rb index 3109ec16e3..b4436f0343 100644 --- a/lib/irb/command/exit.rb +++ b/lib/irb/command/exit.rb @@ -8,10 +8,8 @@ module IRB category "IRB" description "Exit the current irb session." - def execute(*) + def execute(_arg) IRB.irb_exit - rescue UncaughtThrowError - Kernel.exit end end end diff --git a/lib/irb/command/force_exit.rb b/lib/irb/command/force_exit.rb index c2c5542e24..14086aa849 100644 --- a/lib/irb/command/force_exit.rb +++ b/lib/irb/command/force_exit.rb @@ -8,10 +8,8 @@ module IRB category "IRB" description "Exit the current process." - def execute(*) + def execute(_arg) throw :IRB_EXIT, true - rescue UncaughtThrowError - Kernel.exit! end end end diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb index c9f16e05bc..c2018f9b30 100644 --- a/lib/irb/command/help.rb +++ b/lib/irb/command/help.rb @@ -11,7 +11,7 @@ module IRB if command_name.empty? help_message else - if command_class = ExtendCommandBundle.load_command(command_name) + if command_class = Command.load_command(command_name) command_class.help_message || command_class.description else "Can't find command `#{command_name}`. Please check the command name and try again.\n\n" @@ -23,20 +23,14 @@ module IRB private def help_message - commands_info = IRB::ExtendCommandBundle.all_commands_info + commands_info = IRB::Command.all_commands_info + helper_methods_info = IRB::HelperMethod.all_helper_methods_info commands_grouped_by_categories = commands_info.group_by { |cmd| cmd[:category] } - - user_aliases = irb_context.instance_variable_get(:@user_aliases) - - commands_grouped_by_categories["Aliases"] = user_aliases.map do |alias_name, target| - { display_name: alias_name, description: "Alias for `#{target}`" } - end + commands_grouped_by_categories["Helper methods"] = helper_methods_info if irb_context.with_debugger # Remove the original "Debugging" category commands_grouped_by_categories.delete("Debugging") - # Add an empty "Debugging (from debug.gem)" category at the end - commands_grouped_by_categories["Debugging (from debug.gem)"] = [] end longest_cmd_name_length = commands_info.map { |c| c[:display_name].length }.max @@ -44,15 +38,31 @@ module IRB output = StringIO.new 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| + { display_name: alias_name, description: "Alias for `#{target}`" } + end + # Display help commands first add_category_to_output("Help", help_cmds, output, longest_cmd_name_length) + # Display the rest of the commands grouped by categories commands_grouped_by_categories.each do |category, cmds| add_category_to_output(category, cmds, output, longest_cmd_name_length) end + # Display commands without a category + if no_category_cmds + add_category_to_output("No category", no_category_cmds, output, longest_cmd_name_length) + end + + # Display aliases + add_category_to_output("Aliases", aliases, output, longest_cmd_name_length) + # Append the debugger help at the end if irb_context.with_debugger + # Add "Debugging (from debug.gem)" category as title + add_category_to_output("Debugging (from debug.gem)", [], output, longest_cmd_name_length) output.puts DEBUGGER__.help end diff --git a/lib/irb/command/subirb.rb b/lib/irb/command/subirb.rb index 138d61c930..85af28c1a5 100644 --- a/lib/irb/command/subirb.rb +++ b/lib/irb/command/subirb.rb @@ -49,6 +49,7 @@ module IRB extend_irb_context IRB.irb(nil, *obj) + puts IRB.JobManager.inspect end end @@ -65,7 +66,7 @@ module IRB end extend_irb_context - IRB.JobManager + puts IRB.JobManager.inspect end end @@ -90,6 +91,7 @@ module IRB raise CommandArgumentError.new("Please specify the id of target IRB job (listed in the `jobs` command).") unless key IRB.JobManager.switch(key) + puts IRB.JobManager.inspect end end @@ -112,6 +114,7 @@ module IRB extend_irb_context IRB.JobManager.kill(*keys) + puts IRB.JobManager.inspect end end end diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index 8a1df11561..a3d89373c3 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -88,7 +88,7 @@ module IRB def command_completions(preposing, target) if preposing.empty? && !target.empty? - IRB::ExtendCommandBundle.command_names.select { _1.start_with?(target) } + IRB::Command.command_names.select { _1.start_with?(target) } else [] end diff --git a/lib/irb/context.rb b/lib/irb/context.rb index 836b8d2625..aafce7aade 100644 --- a/lib/irb/context.rb +++ b/lib/irb/context.rb @@ -73,11 +73,12 @@ module IRB self.prompt_mode = IRB.conf[:PROMPT_MODE] - if IRB.conf[:SINGLE_IRB] or !defined?(IRB::JobManager) - @irb_name = IRB.conf[:IRB_NAME] - else - @irb_name = IRB.conf[:IRB_NAME]+"#"+IRB.JobManager.n_jobs.to_s + @irb_name = IRB.conf[:IRB_NAME] + + unless IRB.conf[:SINGLE_IRB] or !defined?(IRB::JobManager) + @irb_name = @irb_name + "#" + IRB.JobManager.n_jobs.to_s end + self.irb_path = "(" + @irb_name + ")" case input_method @@ -85,7 +86,7 @@ module IRB @io = nil case use_multiline? when nil - if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline? + if term_interactive? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline? # Both of multiline mode and singleline mode aren't specified. @io = RelineInputMethod.new(build_completor) else @@ -99,7 +100,7 @@ module IRB unless @io case use_singleline? when nil - if (defined?(ReadlineInputMethod) && STDIN.tty? && + if (defined?(ReadlineInputMethod) && term_interactive? && IRB.conf[:PROMPT_MODE] != :INF_RUBY) @io = ReadlineInputMethod.new else @@ -151,6 +152,11 @@ module IRB @command_aliases = @user_aliases.merge(KEYWORD_ALIASES) end + private def term_interactive? + return true if ENV['TEST_IRB_FORCE_INTERACTIVE'] + 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 @@ -587,18 +593,23 @@ module IRB def evaluate(statement, line_no) # :nodoc: @line_no = line_no - result = nil case statement when Statement::EmptyInput return when Statement::Expression result = evaluate_expression(statement.code, line_no) + set_last_value(result) when Statement::Command - result = statement.command_class.execute(self, statement.arg) + statement.command_class.execute(self, statement.arg) + set_last_value(nil) end - set_last_value(result) + nil + end + + def from_binding? + @irb.from_binding end def evaluate_expression(code, line_no) # :nodoc: diff --git a/lib/irb/default_commands.rb b/lib/irb/default_commands.rb index d680655fee..1bbc68efa7 100644 --- a/lib/irb/default_commands.rb +++ b/lib/irb/default_commands.rb @@ -30,35 +30,88 @@ require_relative "command/whereami" require_relative "command/history" module IRB - ExtendCommand = Command - - # Installs the default irb extensions command bundle. - module ExtendCommandBundle - # See #install_alias_method. + module Command NO_OVERRIDE = 0 - # See #install_alias_method. OVERRIDE_PRIVATE_ONLY = 0x01 - # See #install_alias_method. OVERRIDE_ALL = 0x02 - Command._register_with_aliases(:irb_context, Command::Context, - [ - [:context, NO_OVERRIDE], - [:conf, NO_OVERRIDE], - ], + class << self + # This API is for IRB's internal use only and may change at any time. + # Please do NOT use it. + def _register_with_aliases(name, command_class, *aliases) + @commands[name.to_sym] = [command_class, aliases] + end + + def all_commands_info + user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| + result[target] ||= [] + result[target] << alias_name + end + + commands.map do |command_name, (command_class, aliases)| + aliases = aliases.map { |a| a.first } + + if additional_aliases = user_aliases[command_name] + aliases += additional_aliases + end + + display_name = aliases.shift || command_name + { + display_name: display_name, + description: command_class.description, + category: command_class.category + } + end + end + + def command_override_policies + @@command_override_policies ||= commands.flat_map do |cmd_name, (cmd_class, aliases)| + [[cmd_name, OVERRIDE_ALL]] + aliases + end.to_h + end + + def execute_as_command?(name, public_method:, private_method:) + case command_override_policies[name] + when OVERRIDE_ALL + true + when OVERRIDE_PRIVATE_ONLY + !public_method + when NO_OVERRIDE + !public_method && !private_method + end + end + + def command_names + command_override_policies.keys.map(&:to_s) + end + + # Convert a command name to its implementation class if such command exists + def load_command(command) + command = command.to_sym + commands.each do |command_name, (command_class, aliases)| + if command_name == command || aliases.any? { |alias_name, _| alias_name == command } + return command_class + end + end + nil + end + end + + _register_with_aliases(:irb_context, Command::Context, + [:context, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_exit, Command::Exit, + _register_with_aliases(:irb_exit, Command::Exit, [:exit, OVERRIDE_PRIVATE_ONLY], [:quit, OVERRIDE_PRIVATE_ONLY], [:irb_quit, OVERRIDE_PRIVATE_ONLY] ) - Command._register_with_aliases(:irb_exit!, Command::ForceExit, + _register_with_aliases(:irb_exit!, Command::ForceExit, [:exit!, OVERRIDE_PRIVATE_ONLY] ) - Command._register_with_aliases(:irb_current_working_workspace, Command::CurrentWorkingWorkspace, + _register_with_aliases(:irb_current_working_workspace, Command::CurrentWorkingWorkspace, [:cwws, NO_OVERRIDE], [:pwws, NO_OVERRIDE], [:irb_print_working_workspace, OVERRIDE_ALL], @@ -70,7 +123,7 @@ module IRB [:irb_pwb, OVERRIDE_ALL], ) - Command._register_with_aliases(:irb_change_workspace, Command::ChangeWorkspace, + _register_with_aliases(:irb_change_workspace, Command::ChangeWorkspace, [:chws, NO_OVERRIDE], [:cws, NO_OVERRIDE], [:irb_chws, OVERRIDE_ALL], @@ -80,13 +133,13 @@ module IRB [:cb, NO_OVERRIDE], ) - Command._register_with_aliases(:irb_workspaces, Command::Workspaces, + _register_with_aliases(:irb_workspaces, Command::Workspaces, [:workspaces, NO_OVERRIDE], [:irb_bindings, OVERRIDE_ALL], [:bindings, NO_OVERRIDE], ) - Command._register_with_aliases(:irb_push_workspace, Command::PushWorkspace, + _register_with_aliases(:irb_push_workspace, Command::PushWorkspace, [:pushws, NO_OVERRIDE], [:irb_pushws, OVERRIDE_ALL], [:irb_push_binding, OVERRIDE_ALL], @@ -94,7 +147,7 @@ module IRB [:pushb, NO_OVERRIDE], ) - Command._register_with_aliases(:irb_pop_workspace, Command::PopWorkspace, + _register_with_aliases(:irb_pop_workspace, Command::PopWorkspace, [:popws, NO_OVERRIDE], [:irb_popws, OVERRIDE_ALL], [:irb_pop_binding, OVERRIDE_ALL], @@ -102,140 +155,98 @@ module IRB [:popb, NO_OVERRIDE], ) - Command._register_with_aliases(:irb_load, Command::Load) - Command._register_with_aliases(:irb_require, Command::Require) - Command._register_with_aliases(:irb_source, Command::Source, + _register_with_aliases(:irb_load, Command::Load) + _register_with_aliases(:irb_require, Command::Require) + _register_with_aliases(:irb_source, Command::Source, [:source, NO_OVERRIDE] ) - Command._register_with_aliases(:irb, Command::IrbCommand) - Command._register_with_aliases(:irb_jobs, Command::Jobs, + _register_with_aliases(:irb, Command::IrbCommand) + _register_with_aliases(:irb_jobs, Command::Jobs, [:jobs, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_fg, Command::Foreground, + _register_with_aliases(:irb_fg, Command::Foreground, [:fg, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_kill, Command::Kill, + _register_with_aliases(:irb_kill, Command::Kill, [:kill, OVERRIDE_PRIVATE_ONLY] ) - Command._register_with_aliases(:irb_debug, Command::Debug, + _register_with_aliases(:irb_debug, Command::Debug, [:debug, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_edit, Command::Edit, + _register_with_aliases(:irb_edit, Command::Edit, [:edit, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_break, Command::Break) - Command._register_with_aliases(:irb_catch, Command::Catch) - Command._register_with_aliases(:irb_next, Command::Next) - Command._register_with_aliases(:irb_delete, Command::Delete, + _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_delete, Command::Delete, [:delete, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_step, Command::Step, + _register_with_aliases(:irb_step, Command::Step, [:step, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_continue, Command::Continue, + _register_with_aliases(:irb_continue, Command::Continue, [:continue, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_finish, Command::Finish, + _register_with_aliases(:irb_finish, Command::Finish, [:finish, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_backtrace, Command::Backtrace, + _register_with_aliases(:irb_backtrace, Command::Backtrace, [:backtrace, NO_OVERRIDE], [:bt, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_debug_info, Command::Info, + _register_with_aliases(:irb_debug_info, Command::Info, [:info, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_help, Command::Help, + _register_with_aliases(:irb_help, Command::Help, [:help, NO_OVERRIDE], [:show_cmds, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_show_doc, Command::ShowDoc, + _register_with_aliases(:irb_show_doc, Command::ShowDoc, [:show_doc, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_info, Command::IrbInfo) + _register_with_aliases(:irb_info, Command::IrbInfo) - Command._register_with_aliases(:irb_ls, Command::Ls, + _register_with_aliases(:irb_ls, Command::Ls, [:ls, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_measure, Command::Measure, + _register_with_aliases(:irb_measure, Command::Measure, [:measure, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_show_source, Command::ShowSource, + _register_with_aliases(:irb_show_source, Command::ShowSource, [:show_source, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_whereami, Command::Whereami, + _register_with_aliases(:irb_whereami, Command::Whereami, [:whereami, NO_OVERRIDE] ) - Command._register_with_aliases(:irb_history, Command::History, + _register_with_aliases(:irb_history, Command::History, [:history, NO_OVERRIDE], [:hist, NO_OVERRIDE] ) + end - def self.all_commands_info - user_aliases = IRB.CurrentContext.command_aliases.each_with_object({}) do |(alias_name, target), result| - result[target] ||= [] - result[target] << alias_name - end - - Command.commands.map do |command_name, (command_class, aliases)| - aliases = aliases.map { |a| a.first } - - if additional_aliases = user_aliases[command_name] - aliases += additional_aliases - end - - display_name = aliases.shift || command_name - { - display_name: display_name, - description: command_class.description, - category: command_class.category - } - end - end - - def self.command_override_policies - @@command_override_policies ||= Command.commands.flat_map do |cmd_name, (cmd_class, aliases)| - [[cmd_name, OVERRIDE_ALL]] + aliases - end.to_h - end - - def self.execute_as_command?(name, public_method:, private_method:) - case command_override_policies[name] - when OVERRIDE_ALL - true - when OVERRIDE_PRIVATE_ONLY - !public_method - when NO_OVERRIDE - !public_method && !private_method - end - end - - def self.command_names - command_override_policies.keys.map(&:to_s) - end + ExtendCommand = Command - # Convert a command name to its implementation class if such command exists - def self.load_command(command) - command = command.to_sym - Command.commands.each do |command_name, (command_class, aliases)| - if command_name == command || aliases.any? { |alias_name, _| alias_name == command } - return command_class - end - end - nil - end + # For backward compatibility, we need to keep this module: + # - As a container of helper methods + # - As a place to register commands with the deprecated def_extend_command method + module ExtendCommandBundle + # For backward compatibility + NO_OVERRIDE = Command::NO_OVERRIDE + OVERRIDE_PRIVATE_ONLY = Command::OVERRIDE_PRIVATE_ONLY + OVERRIDE_ALL = Command::OVERRIDE_ALL # Deprecated. Doesn't have any effect. @EXTEND_COMMANDS = [] @@ -243,7 +254,7 @@ module IRB # 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_override_policies = nil + Command.class_variable_set(:@@command_override_policies, nil) end end end diff --git a/lib/irb/helper_method.rb b/lib/irb/helper_method.rb new file mode 100644 index 0000000000..f1f6fff915 --- /dev/null +++ b/lib/irb/helper_method.rb @@ -0,0 +1,29 @@ +require_relative "helper_method/base" + +module IRB + module HelperMethod + @helper_methods = {} + + class << self + attr_reader :helper_methods + + def register(name, helper_class) + @helper_methods[name] = helper_class + + if defined?(HelpersContainer) + HelpersContainer.install_helper_methods + end + end + + def all_helper_methods_info + @helper_methods.map do |name, helper_class| + { display_name: name, description: helper_class.description } + end + end + end + + # Default helper_methods + require_relative "helper_method/conf" + register(:conf, HelperMethod::Conf) + end +end diff --git a/lib/irb/helper_method/base.rb b/lib/irb/helper_method/base.rb new file mode 100644 index 0000000000..a68001ed28 --- /dev/null +++ b/lib/irb/helper_method/base.rb @@ -0,0 +1,16 @@ +require "singleton" + +module IRB + module HelperMethod + class Base + include Singleton + + class << self + def description(description = nil) + @description = description if description + @description + end + end + end + end +end diff --git a/lib/irb/helper_method/conf.rb b/lib/irb/helper_method/conf.rb new file mode 100644 index 0000000000..718ed279c0 --- /dev/null +++ b/lib/irb/helper_method/conf.rb @@ -0,0 +1,11 @@ +module IRB + module HelperMethod + class Conf < Base + description "Returns the current IRB context." + + def execute + IRB.CurrentContext + end + end + end +end diff --git a/lib/irb/init.rb b/lib/irb/init.rb index 355047519c..7dc08912ef 100644 --- a/lib/irb/init.rb +++ b/lib/irb/init.rb @@ -52,6 +52,7 @@ module IRB # :nodoc: IRB.init_error IRB.parse_opts(argv: argv) IRB.run_config + IRB.validate_config IRB.load_modules unless @CONF[:PROMPT][@CONF[:PROMPT_MODE]] @@ -427,6 +428,40 @@ module IRB # :nodoc: @irbrc_files end + def IRB.validate_config + conf[:IRB_NAME] = conf[:IRB_NAME].to_s + + irb_rc = conf[:IRB_RC] + unless irb_rc.nil? || irb_rc.respond_to?(:call) + raise_validation_error "IRB.conf[:IRB_RC] should be a callable object. Got #{irb_rc.inspect}." + end + + back_trace_limit = conf[:BACK_TRACE_LIMIT] + unless back_trace_limit.is_a?(Integer) + raise_validation_error "IRB.conf[:BACK_TRACE_LIMIT] should be an integer. Got #{back_trace_limit.inspect}." + end + + prompt = conf[:PROMPT] + unless prompt.is_a?(Hash) + msg = "IRB.conf[:PROMPT] should be a Hash. Got #{prompt.inspect}." + + if prompt.is_a?(Symbol) + msg += " Did you mean to set `IRB.conf[:PROMPT_MODE]`?" + end + + raise_validation_error msg + end + + eval_history = conf[:EVAL_HISTORY] + unless eval_history.nil? || eval_history.is_a?(Integer) + raise_validation_error "IRB.conf[:EVAL_HISTORY] should be an integer. Got #{eval_history.inspect}." + end + end + + def IRB.raise_validation_error(msg) + raise TypeError, msg, @irbrc_files + end + # loading modules def IRB.load_modules for m in @CONF[:LOAD_MODULES] diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index e5adb350e8..684527edc4 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -67,6 +67,7 @@ module IRB # # See IO#gets for more information. def gets + puts if @stdout.tty? # workaround for debug compatibility test print @prompt line = @stdin.gets @line[@line_no += 1] = line diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb index cfe36be83f..86e340eb05 100644 --- a/lib/irb/ruby-lex.rb +++ b/lib/irb/ruby-lex.rb @@ -230,7 +230,7 @@ module IRB # example: # ' return :recoverable_error - when /syntax error, unexpected end-of-input/ + when /unexpected end-of-input/ # "syntax error, unexpected end-of-input, expecting keyword_end" # # example: @@ -240,7 +240,7 @@ module IRB # fuga # end return :recoverable_error - when /syntax error, unexpected keyword_end/ + when /unexpected keyword_end/ # "syntax error, unexpected keyword_end" # # example: @@ -250,7 +250,7 @@ module IRB # example: # end return :unrecoverable_error - when /syntax error, unexpected '\.'/ + when /unexpected '\.'/ # "syntax error, unexpected '.'" # # example: diff --git a/lib/irb/version.rb b/lib/irb/version.rb index 9a7b12766b..c41917329c 100644 --- a/lib/irb/version.rb +++ b/lib/irb/version.rb @@ -5,7 +5,7 @@ # module IRB # :nodoc: - VERSION = "1.12.0" + VERSION = "1.13.1" @RELEASE_VERSION = VERSION - @LAST_UPDATE_DATE = "2024-03-06" + @LAST_UPDATE_DATE = "2024-05-05" end diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb index 1490f7b478..d24d1cc38d 100644 --- a/lib/irb/workspace.rb +++ b/lib/irb/workspace.rb @@ -6,6 +6,8 @@ require "delegate" +require_relative "helper_method" + IRB::TOPLEVEL_BINDING = binding module IRB # :nodoc: class WorkSpace @@ -109,9 +111,9 @@ EOF attr_reader :main def load_helper_methods_to_main - if !(class<<main;ancestors;end).include?(ExtendCommandBundle) - main.extend ExtendCommandBundle - end + ancestors = class<<main;ancestors;end + main.extend ExtendCommandBundle if !ancestors.include?(ExtendCommandBundle) + main.extend HelpersContainer if !ancestors.include?(HelpersContainer) end # Evaluate the given +statements+ within the context of this workspace. @@ -172,4 +174,16 @@ EOF "\nFrom: #{file} @ line #{pos + 1} :\n\n#{body}#{Color.clear}\n" end 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) + end + end + + install_helper_methods + end end diff --git a/lib/prism.rb b/lib/prism.rb index c512cb4015..2bb7f79bf6 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -18,10 +18,10 @@ module Prism autoload :Dispatcher, "prism/dispatcher" autoload :DotVisitor, "prism/dot_visitor" autoload :DSL, "prism/dsl" + autoload :InspectVisitor, "prism/inspect_visitor" autoload :LexCompat, "prism/lex_compat" autoload :LexRipper, "prism/lex_compat" autoload :MutationCompiler, "prism/mutation_compiler" - autoload :NodeInspector, "prism/node_inspector" autoload :Pack, "prism/pack" autoload :Pattern, "prism/pattern" autoload :Reflection, "prism/reflection" @@ -37,7 +37,7 @@ module Prism private_constant :LexRipper # :call-seq: - # Prism::lex_compat(source, **options) -> ParseResult + # Prism::lex_compat(source, **options) -> LexCompat::Result # # Returns a parse result whose value is an array of tokens that closely # resembles the return value of Ripper::lex. The main difference is that the @@ -67,11 +67,10 @@ module Prism end end +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/desugar_compiler.rb b/lib/prism/desugar_compiler.rb index 9b62c00df3..de02445149 100644 --- a/lib/prism/desugar_compiler.rb +++ b/lib/prism/desugar_compiler.rb @@ -73,7 +73,7 @@ module Prism # Desugar `x += y` to `x = x + y` def compile - operator_loc = node.operator_loc.chop + binary_operator_loc = node.binary_operator_loc.chop write_class.new( source, @@ -84,15 +84,15 @@ module Prism 0, read_class.new(source, *arguments, node.name_loc), nil, - operator_loc.slice.to_sym, - operator_loc, + binary_operator_loc.slice.to_sym, + binary_operator_loc, nil, ArgumentsNode.new(source, 0, [node.value], node.value.location), nil, nil, node.location ), - node.operator_loc.copy(start_offset: node.operator_loc.end_offset - 1, length: 1), + node.binary_operator_loc.copy(start_offset: node.binary_operator_loc.end_offset - 1, length: 1), node.location ) end diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index 1fd053f902..b62a59d037 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -317,7 +317,7 @@ module Prism buffer.read end - Serialize.load_tokens(Source.new(code), serialized) + Serialize.load_tokens(Source.for(code), serialized) end def parse_common(string, code, options) # :nodoc: @@ -329,7 +329,7 @@ module Prism LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse_comments(buffer.pointer, string.pointer, string.length, dump_options(options)) - source = Source.new(code) + source = Source.for(code) loader = Serialize::Loader.new(source, buffer.read) loader.load_header @@ -343,14 +343,14 @@ module Prism LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse_lex(buffer.pointer, string.pointer, string.length, dump_options(options)) - source = Source.new(code) + source = Source.for(code) loader = Serialize::Loader.new(source, buffer.read) tokens = loader.load_tokens node, comments, magic_comments, data_loc, errors, warnings = loader.load_nodes tokens.each { |token,| token.value.force_encoding(loader.encoding) } - ParseResult.new([node, tokens], comments, magic_comments, data_loc, errors, warnings, source) + ParseLexResult.new([node, tokens], comments, magic_comments, data_loc, errors, warnings, source) end end @@ -408,7 +408,7 @@ module Prism values << dump_options_command_line(options) template << "C" - values << { nil => 0, "3.3.0" => 1, "3.4.0" => 0, "latest" => 0 }.fetch(options[:version]) + values << { nil => 0, "3.3.0" => 1, "3.3.1" => 1, "3.4.0" => 0, "latest" => 0 }.fetch(options[:version]) template << "L" if (scopes = options[:scopes]) diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 70cb065201..4f8e443a3b 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -10,6 +10,23 @@ module Prism # generally lines up. However, there are a few cases that require special # handling. class LexCompat # :nodoc: + # A result class specialized for holding tokens produced by the lexer. + class Result < Prism::Result + # The list of tokens that were produced by the lexer. + attr_reader :value + + # Create a new lex compat result object with the given values. + def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + @value = value + super(comments, magic_comments, data_loc, errors, warnings, source) + end + + # Implement the hash pattern matching interface for Result. + def deconstruct_keys(keys) + super.merge!(value: value) + end + end + # This is a mapping of prism token types to Ripper token types. This is a # many-to-one mapping because we split up our token types, whereas Ripper # tends to group them. @@ -844,7 +861,7 @@ module Prism # We sort by location to compare against Ripper's output tokens.sort_by!(&:location) - ParseResult.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, Source.new(source)) + Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, Source.for(source)) end end diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index 8674544065..ceec76b8d6 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -3,6 +3,17 @@ # Here we are reopening the prism module to provide methods on nodes that aren't # templated and are meant as convenience methods. module Prism + class Node + def deprecated(*replacements) # :nodoc: + 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. + #{(caller(1, 3) || []).join("\n")} + MSG + end + end + module RegularExpressionOptions # :nodoc: # Returns a numeric value that represents the flags that were used to create # the regular expression. @@ -143,11 +154,12 @@ module Prism current = self #: node? while current.is_a?(ConstantPathNode) - child = current.child - if child.is_a?(MissingNode) + name = current.name + if name.nil? raise MissingNodesInConstantPathError, "Constant path contains missing nodes. Cannot compute full name" end - parts.unshift(child.name) + + parts.unshift(name) current = current.parent end @@ -162,6 +174,14 @@ module Prism def full_name full_name_parts.join("::") end + + # Previously, we had a child node on this class that contained either a + # constant read or a missing node. To not cause a breaking change, we + # continue to supply that API. + def child + deprecated("name", "name_loc") + name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location) + end end class ConstantPathTargetNode < Node @@ -179,17 +199,25 @@ module Prism raise ConstantPathNode::DynamicPartsInConstantPathError, "Constant target path contains dynamic parts. Cannot compute full name" end - if child.is_a?(MissingNode) + if name.nil? raise ConstantPathNode::MissingNodesInConstantPathError, "Constant target path contains missing nodes. Cannot compute full name" end - parts.push(child.name) + parts.push(name) end # Returns the full name of this constant path. For example: "Foo::Bar" def full_name full_name_parts.join("::") end + + # Previously, we had a child node on this class that contained either a + # constant read or a missing node. To not cause a breaking change, we + # continue to supply that API. + def child + deprecated("name", "name_loc") + name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location) + end end class ConstantTargetNode < Node @@ -257,4 +285,147 @@ module Prism names end end + + class CallNode < Node + # When a call node has the attribute_write flag set, it means that the call + # is using the attribute write syntax. This is either a method call to []= + # or a method call to a method that ends with =. Either way, the = sign is + # present in the source. + # + # Prism returns the message_loc _without_ the = sign attached, because there + # can be any amount of space between the message and the = sign. However, + # sometimes you want the location of the full message including the inner + # space and the = sign. This method provides that. + def full_message_loc + attribute_write? ? message_loc&.adjoin("=") : message_loc + end + end + + class CallOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class ClassVariableOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class ConstantOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class ConstantPathOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class GlobalVariableOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class IndexOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class InstanceVariableOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end + + class LocalVariableOperatorWriteNode < Node + # Returns the binary operator used to modify the receiver. This method is + # deprecated in favor of #binary_operator. + def operator + deprecated("binary_operator") + binary_operator + end + + # Returns the location of the binary operator used to modify the receiver. + # This method is deprecated in favor of #binary_operator_loc. + def operator_loc + deprecated("binary_operator_loc") + binary_operator_loc + end + end end diff --git a/lib/prism/node_inspector.rb b/lib/prism/node_inspector.rb deleted file mode 100644 index d77af33c3a..0000000000 --- a/lib/prism/node_inspector.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -module Prism - # This object is responsible for generating the output for the inspect method - # implementations of child nodes. - class NodeInspector # :nodoc: - attr_reader :prefix, :output - - def initialize(prefix = "") - @prefix = prefix - @output = +"" - end - - # Appends a line to the output with the current prefix. - def <<(line) - output << "#{prefix}#{line}" - end - - # This generates a string that is used as the header of the inspect output - # for any given node. - def header(node) - output = +"@ #{node.class.name.split("::").last} (" - output << "location: (#{node.location.start_line},#{node.location.start_column})-(#{node.location.end_line},#{node.location.end_column})" - output << ", newline: true" if node.newline? - output << ")\n" - output - end - - # Generates a string that represents a list of nodes. It handles properly - # using the box drawing characters to make the output look nice. - def list(prefix, nodes) - output = +"(length: #{nodes.length})\n" - last_index = nodes.length - 1 - - nodes.each_with_index do |node, index| - pointer, preadd = (index == last_index) ? ["└── ", " "] : ["├── ", "│ "] - node_prefix = "#{prefix}#{preadd}" - output << node.inspect(NodeInspector.new(node_prefix)).sub(node_prefix, "#{prefix}#{pointer}") - end - - output - end - - # Generates a string that represents a location field on a node. - def location(value) - if value - "(#{value.start_line},#{value.start_column})-(#{value.end_line},#{value.end_column}) = #{value.slice.inspect}" - else - "∅" - end - end - - # Generates a string that represents a child node. - def child_node(node, append) - node.inspect(child_inspector(append)).delete_prefix(prefix) - end - - # Returns a new inspector that can be used to inspect a child node. - def child_inspector(append) - NodeInspector.new("#{prefix}#{append}") - end - - # Returns the output as a string. - def to_str - output - end - end -end diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 39e15f6027..798fde09e5 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -5,6 +5,14 @@ module Prism # conjunction with locations to allow them to resolve line numbers and source # ranges. class Source + # Create a new source object with the given source code. This method should + # be used instead of `new` and it will return either a `Source` or a + # 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) + end + # The source code that this source object represents. attr_reader :source @@ -27,6 +35,11 @@ module Prism source.encoding end + # Returns the lines of the source code as an array of strings. + def lines + source.lines + end + # Perform a byteslice on the source code using the given byte offset and # byte length. def slice(byte_offset, length) @@ -45,6 +58,12 @@ module Prism offsets[find_line(byte_offset)] end + # Returns the byte offset of the end of the line corresponding to the given + # byte offset. + def line_end(byte_offset) + offsets[find_line(byte_offset) + 1] || source.bytesize + end + # Return the column number for the given byte offset. def column(byte_offset) byte_offset - line_start(byte_offset) @@ -100,6 +119,39 @@ module Prism end end + # Specialized version of Prism::Source for source code that includes ASCII + # characters only. This class is used to apply performance optimizations that + # cannot be applied to sources that include multibyte characters. Sources that + # include multibyte characters are represented by the Prism::Source class. + class ASCIISource < Source + # Return the character offset for the given byte offset. + def character_offset(byte_offset) + byte_offset + end + + # Return the column number in characters for the given byte offset. + def character_column(byte_offset) + byte_offset - line_start(byte_offset) + end + + # Returns the offset from the start of the file for the given byte offset + # counting in code units for the given encoding. + # + # This method is tested with UTF-8, UTF-16, and UTF-32. If there is the + # concept of code units that differs from the number of characters in other + # encodings, it is not captured here. + def code_units_offset(byte_offset, encoding) + byte_offset + end + + # 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`. + def code_units_column(byte_offset, encoding) + byte_offset - line_start(byte_offset) + end + end + # This represents a location in the source. class Location # A Source object that is used to determine more information from the given @@ -171,11 +223,25 @@ module Prism "#<Prism::Location @start_offset=#{@start_offset} @length=#{@length} start_line=#{start_line}>" end + # Returns all of the lines of the source code associated with this location. + def source_lines + source.lines + end + # The source code that this location represents. def slice source.slice(start_offset, length) end + # The source code that this location represents starting from the beginning + # of the line that this location starts on to the end of the line that this + # location ends on. + def slice_lines + line_start = source.line_start(start_offset) + line_end = source.line_end(end_offset) + source.slice(line_start, line_end - line_start) + end + # The character offset from the beginning of the source where this location # starts. def start_character_offset @@ -281,6 +347,18 @@ module Prism Location.new(source, start_offset, other.end_offset - start_offset) end + + # Join this location with the first occurrence of the string in the source + # that occurs after this location on the same line, and return the new + # location. This will raise an error if the string does not exist. + def adjoin(string) + line_suffix = source.slice(end_offset, source.line_end(end_offset) - end_offset) + + line_suffix_index = line_suffix.byteindex(string) + raise "Could not find #{string}" if line_suffix_index.nil? + + Location.new(source, start_offset, length + line_suffix_index + string.bytesize) + end end # This represents a comment that was encountered during parsing. It is the @@ -438,14 +516,9 @@ module Prism end # This represents the result of a call to ::parse or ::parse_file. It contains - # the AST, any comments that were encounters, and any errors that were - # encountered. - class ParseResult - # The value that was generated by parsing. Normally this holds the AST, but - # it can sometimes how a list of tokens or other results passed back from - # the parser. - attr_reader :value - + # the requested structure, any comments that were encounters, and any errors + # that were encountered. + class Result # The list of comments that were encountered during parsing. attr_reader :comments @@ -466,9 +539,8 @@ module Prism # A Source instance that represents the source code that was parsed. attr_reader :source - # Create a new parse result object with the given values. - def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) - @value = value + # Create a new result object with the given values. + def initialize(comments, magic_comments, data_loc, errors, warnings, source) @comments = comments @magic_comments = magic_comments @data_loc = data_loc @@ -477,9 +549,9 @@ module Prism @source = source end - # Implement the hash pattern matching interface for ParseResult. + # Implement the hash pattern matching interface for Result. def deconstruct_keys(keys) - { value: value, comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings } + { comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings } end # Returns the encoding of the source code that was parsed. @@ -500,6 +572,75 @@ module Prism end end + # This is a result specific to the `parse` and `parse_file` methods. + class ParseResult < Result + autoload :Comments, "prism/parse_result/comments" + autoload :Newlines, "prism/parse_result/newlines" + + private_constant :Comments + private_constant :Newlines + + # The syntax tree that was parsed from the source code. + attr_reader :value + + # Create a new parse result object with the given values. + def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + @value = value + super(comments, magic_comments, data_loc, errors, warnings, source) + end + + # Implement the hash pattern matching interface for ParseResult. + 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 + end + + # This is a result specific to the `lex` and `lex_file` methods. + class LexResult < Result + # The list of tokens that were parsed from the source code. + attr_reader :value + + # Create a new lex result object with the given values. + def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + @value = value + super(comments, magic_comments, data_loc, errors, warnings, source) + end + + # Implement the hash pattern matching interface for LexResult. + def deconstruct_keys(keys) + super.merge!(value: value) + end + end + + # This is a result specific to the `parse_lex` and `parse_lex_file` methods. + class ParseLexResult < Result + # A tuple of the syntax tree and the list of tokens that were parsed from + # the source code. + attr_reader :value + + # Create a new parse lex result object with the given values. + def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + @value = value + super(comments, magic_comments, data_loc, errors, warnings, source) + end + + # Implement the hash pattern matching interface for ParseLexResult. + def deconstruct_keys(keys) + super.merge!(value: value) + end + end + # This represents a token from the Ruby source. class Token # The Source object that represents the source this token came from. diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb index f8f74d2503..22c4148b2c 100644 --- a/lib/prism/parse_result/comments.rb +++ b/lib/prism/parse_result/comments.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Prism - class ParseResult + class ParseResult < Result # When we've parsed the source, we have both the syntax tree and the list of # comments that we found in the source. This class is responsible for # walking the tree and finding the nearest location to attach each comment. @@ -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/newlines.rb b/lib/prism/parse_result/newlines.rb index 03acb0b862..cc1343dfda 100644 --- a/lib/prism/parse_result/newlines.rb +++ b/lib/prism/parse_result/newlines.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Prism - class ParseResult + class ParseResult < Result # The :line tracepoint event gets fired whenever the Ruby VM encounters an # expression on a new line. The types of expressions that can trigger this # event are: @@ -17,21 +17,26 @@ 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) + @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 +44,7 @@ module Prism # Mark if/unless nodes as newlines. def visit_if_node(node) - node.set_newline_flag(@newline_marked) + node.newline!(@lines) super(node) end @@ -48,19 +53,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!(@lines) end super(node) end end + end + + class Node + def newline? # :nodoc: + @newline ? true : false + end + + def newline!(lines) # :nodoc: + line = location.start_line + unless lines[line] + lines[line] = true + @newline = true + end + end + end + + class BeginNode < Node + def newline!(lines) # :nodoc: + # Never mark BeginNode with a newline flag, mark children instead. + end + end + + class ParenthesesNode < Node + def newline!(lines) # :nodoc: + # Never mark ParenthesesNode with a newline flag, mark children instead. + end + end + + class IfNode < Node + def newline!(lines) # :nodoc: + predicate.newline!(lines) + end + end + + class UnlessNode < Node + def newline!(lines) # :nodoc: + predicate.newline!(lines) + end + end + + class UntilNode < Node + def newline!(lines) # :nodoc: + predicate.newline!(lines) + end + end + + class WhileNode < Node + def newline!(lines) # :nodoc: + predicate.newline!(lines) + end + end + + class RescueModifierNode < Node + def newline!(lines) # :nodoc: + expression.newline!(lines) + end + end + + class InterpolatedMatchLastLineNode < Node + def newline!(lines) # :nodoc: + first = parts.first + first.newline!(lines) if first + end + end + + class InterpolatedRegularExpressionNode < Node + def newline!(lines) # :nodoc: + first = parts.first + first.newline!(lines) if first + end + end + + class InterpolatedStringNode < Node + def newline!(lines) # :nodoc: + first = parts.first + first.newline!(lines) if first + end + end - private_constant :Newlines + class InterpolatedSymbolNode < Node + def newline!(lines) # :nodoc: + first = parts.first + first.newline!(lines) if first + end + end - # Walk the tree and mark nodes that are on a new line. - def mark_newlines! - value = self.value - raise "This method should only be called on a parse result that contains a node" unless Node === value - value.accept(Newlines.new(Array.new(1 + source.offsets.size, false))) # steep:ignore + class InterpolatedXStringNode < Node + def newline!(lines) # :nodoc: + first = parts.first + first.newline!(lines) if first end end end diff --git a/lib/prism/pattern.rb b/lib/prism/pattern.rb index e12cfd597f..03fec26789 100644 --- a/lib/prism/pattern.rb +++ b/lib/prism/pattern.rb @@ -149,7 +149,10 @@ module Prism parent = node.parent if parent.is_a?(ConstantReadNode) && parent.slice == "Prism" - compile_node(node.child) + name = node.name + raise CompilationError, node.inspect if name.nil? + + compile_constant_name(node, name) else compile_error(node) end @@ -158,14 +161,17 @@ module Prism # in ConstantReadNode # in String def compile_constant_read_node(node) - value = node.slice + compile_constant_name(node, node.name) + end - if Prism.const_defined?(value, false) - clazz = Prism.const_get(value) + # Compile a name associated with a constant. + def compile_constant_name(node, name) + if Prism.const_defined?(name, false) + clazz = Prism.const_get(name) ->(other) { clazz === other } - elsif Object.const_defined?(value, false) - clazz = Object.const_get(value) + elsif Object.const_defined?(name, false) + clazz = Object.const_get(name) ->(other) { clazz === other } else diff --git a/lib/prism/polyfill/byteindex.rb b/lib/prism/polyfill/byteindex.rb new file mode 100644 index 0000000000..98c6089f14 --- /dev/null +++ b/lib/prism/polyfill/byteindex.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Polyfill for String#byteindex, which didn't exist until Ruby 3.2. +if !("".respond_to?(:byteindex)) + String.include( + Module.new { + def byteindex(needle, offset = 0) + charindex = index(needle, offset) + slice(0...charindex).bytesize if charindex + end + } + ) +end diff --git a/lib/prism/polyfill/string.rb b/lib/prism/polyfill/string.rb deleted file mode 100644 index 582266d956..0000000000 --- a/lib/prism/polyfill/string.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -# Polyfill for String#unpack1 with the offset parameter. -if String.instance_method(:unpack1).parameters.none? { |_, name| name == :offset } - String.prepend( - Module.new { - def unpack1(format, offset: 0) # :nodoc: - offset == 0 ? super(format) : self[offset..].unpack1(format) # steep:ignore - end - } - ) -end diff --git a/lib/prism/polyfill/unpack1.rb b/lib/prism/polyfill/unpack1.rb new file mode 100644 index 0000000000..3fa9b5a0c5 --- /dev/null +++ b/lib/prism/polyfill/unpack1.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Polyfill for String#unpack1 with the offset parameter. Not all Ruby engines +# have Method#parameters implemented, so we check the arity instead if +# necessary. +if (unpack1 = String.instance_method(:unpack1)).respond_to?(:parameters) ? unpack1.parameters.none? { |_, name| name == :offset } : (unpack1.arity == 1) + String.prepend( + Module.new { + def unpack1(format, offset: 0) # :nodoc: + offset == 0 ? super(format) : self[offset..].unpack1(format) # steep:ignore + end + } + ) +end diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index e55cac6df6..374591bb70 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.25.0" + spec.version = "0.29.0" spec.authors = ["Shopify"] spec.email = ["ruby@shopify.com"] @@ -76,17 +76,18 @@ Gem::Specification.new do |spec| "lib/prism/dot_visitor.rb", "lib/prism/dsl.rb", "lib/prism/ffi.rb", + "lib/prism/inspect_visitor.rb", "lib/prism/lex_compat.rb", "lib/prism/mutation_compiler.rb", "lib/prism/node_ext.rb", - "lib/prism/node_inspector.rb", "lib/prism/node.rb", "lib/prism/pack.rb", "lib/prism/parse_result.rb", "lib/prism/parse_result/comments.rb", "lib/prism/parse_result/newlines.rb", "lib/prism/pattern.rb", - "lib/prism/polyfill/string.rb", + "lib/prism/polyfill/byteindex.rb", + "lib/prism/polyfill/unpack1.rb", "lib/prism/reflection.rb", "lib/prism/serialize.rb", "lib/prism/translation.rb", @@ -101,11 +102,42 @@ Gem::Specification.new do |spec| "lib/prism/translation/ripper/shim.rb", "lib/prism/translation/ruby_parser.rb", "lib/prism/visitor.rb", + "prism.gemspec", + "rbi/prism.rbi", + "rbi/prism/compiler.rbi", + "rbi/prism/inspect_visitor.rbi", + "rbi/prism/node_ext.rbi", + "rbi/prism/node.rbi", + "rbi/prism/parse_result.rbi", + "rbi/prism/reflection.rbi", + "rbi/prism/translation/parser.rbi", + "rbi/prism/translation/parser33.rbi", + "rbi/prism/translation/parser34.rbi", + "rbi/prism/translation/ripper.rbi", + "rbi/prism/visitor.rbi", + "sig/prism.rbs", + "sig/prism/compiler.rbs", + "sig/prism/dispatcher.rbs", + "sig/prism/dot_visitor.rbs", + "sig/prism/dsl.rbs", + "sig/prism/inspect_visitor.rbs", + "sig/prism/lex_compat.rbs", + "sig/prism/mutation_compiler.rbs", + "sig/prism/node_ext.rbs", + "sig/prism/node.rbs", + "sig/prism/pack.rbs", + "sig/prism/parse_result.rbs", + "sig/prism/pattern.rbs", + "sig/prism/reflection.rbs", + "sig/prism/serialize.rbs", + "sig/prism/visitor.rbs", "src/diagnostic.c", "src/encoding.c", "src/node.c", + "src/options.c", "src/pack.c", "src/prettyprint.c", + "src/prism.c", "src/regexp.c", "src/serialize.c", "src/static_literals.c", @@ -117,43 +149,10 @@ 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.c", "src/util/pm_string_list.c", + "src/util/pm_string.c", "src/util/pm_strncasecmp.c", - "src/util/pm_strpbrk.c", - "src/options.c", - "src/prism.c", - "prism.gemspec", - "sig/prism.rbs", - "sig/prism/compiler.rbs", - "sig/prism/dispatcher.rbs", - "sig/prism/dot_visitor.rbs", - "sig/prism/dsl.rbs", - "sig/prism/mutation_compiler.rbs", - "sig/prism/node.rbs", - "sig/prism/node_ext.rbs", - "sig/prism/pack.rbs", - "sig/prism/parse_result.rbs", - "sig/prism/pattern.rbs", - "sig/prism/reflection.rbs", - "sig/prism/serialize.rbs", - "sig/prism/visitor.rbs", - "rbi/prism.rbi", - "rbi/prism/compiler.rbi", - "rbi/prism/desugar_compiler.rbi", - "rbi/prism/mutation_compiler.rbi", - "rbi/prism/node_ext.rbi", - "rbi/prism/node.rbi", - "rbi/prism/parse_result.rbi", - "rbi/prism/reflection.rbi", - "rbi/prism/translation/parser.rbi", - "rbi/prism/translation/parser/compiler.rbi", - "rbi/prism/translation/parser33.rbi", - "rbi/prism/translation/parser34.rbi", - "rbi/prism/translation/ripper.rbi", - "rbi/prism/translation/ripper/ripper_compiler.rbi", - "rbi/prism/translation/ruby_parser.rbi", - "rbi/prism/visitor.rbi" + "src/util/pm_strpbrk.c" ] spec.extensions = ["ext/prism/extconf.rb"] diff --git a/lib/prism/translation/parser.rb b/lib/prism/translation/parser.rb index 0d11b8f566..3748fc896e 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)), offset_cache) + result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), 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)), offset_cache) + result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), 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)), offset_cache) + unwrap(Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache) rescue ::Parser::SyntaxError raise if !recover end @@ -149,17 +154,17 @@ module Prism Diagnostic.new(:error, :endless_setter, {}, diagnostic_location, []) when :embdoc_term Diagnostic.new(:error, :embedded_document, {}, diagnostic_location, []) - when :incomplete_variable_class, :incomplete_variable_class_3_3_0 + when :incomplete_variable_class, :incomplete_variable_class_3_3 location = location.copy(length: location.length + 1) diagnostic_location = build_range(location, offset_cache) Diagnostic.new(:error, :cvar_name, { name: location.slice }, diagnostic_location, []) - when :incomplete_variable_instance, :incomplete_variable_instance_3_3_0 + when :incomplete_variable_instance, :incomplete_variable_instance_3_3 location = location.copy(length: location.length + 1) diagnostic_location = build_range(location, offset_cache) Diagnostic.new(:error, :ivar_name, { name: location.slice }, diagnostic_location, []) - when :invalid_variable_global, :invalid_variable_global_3_3_0 + when :invalid_variable_global, :invalid_variable_global_3_3 Diagnostic.new(:error, :gvar_name, { name: location.slice }, diagnostic_location, []) when :module_in_method Diagnostic.new(:error, :module_in_def, {}, diagnostic_location, []) @@ -284,7 +289,7 @@ module Prism def convert_for_prism(version) case version when 33 - "3.3.0" + "3.3.1" when 34 "3.4.0" else diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb index 9437589623..a6c3118efd 100644 --- a/lib/prism/translation/parser/compiler.rb +++ b/lib/prism/translation/parser/compiler.rb @@ -328,18 +328,48 @@ module Prism [], nil ), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo.bar &&= baz # ^^^^^^^^^^^^^^^ - alias visit_call_and_write_node visit_call_operator_write_node + def visit_call_and_write_node(node) + call_operator_loc = node.call_operator_loc + + builder.op_assign( + builder.call_method( + visit(node.receiver), + call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], + node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, + nil, + [], + nil + ), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo.bar ||= baz # ^^^^^^^^^^^^^^^ - alias visit_call_or_write_node visit_call_operator_write_node + def visit_call_or_write_node(node) + call_operator_loc = node.call_operator_loc + + builder.op_assign( + builder.call_method( + visit(node.receiver), + call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], + node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, + nil, + [], + nil + ), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo.bar, = 1 # ^^^^^^^ @@ -419,18 +449,30 @@ module Prism def visit_class_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @@foo &&= bar # ^^^^^^^^^^^^^ - alias visit_class_variable_and_write_node visit_class_variable_operator_write_node + def visit_class_variable_and_write_node(node) + builder.op_assign( + builder.assignable(builder.cvar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # @@foo ||= bar # ^^^^^^^^^^^^^ - alias visit_class_variable_or_write_node visit_class_variable_operator_write_node + def visit_class_variable_or_write_node(node) + builder.op_assign( + builder.assignable(builder.cvar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # @@foo, = bar # ^^^^^ @@ -458,18 +500,30 @@ module Prism def visit_constant_operator_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo &&= bar # ^^^^^^^^^^^^ - alias visit_constant_and_write_node visit_constant_operator_write_node + def visit_constant_and_write_node(node) + builder.op_assign( + builder.assignable(builder.const([node.name, srange(node.name_loc)])), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # Foo ||= bar # ^^^^^^^^^^^^ - alias visit_constant_or_write_node visit_constant_operator_write_node + def visit_constant_or_write_node(node) + builder.op_assign( + builder.assignable(builder.const([node.name, srange(node.name_loc)])), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # Foo, = bar # ^^^ @@ -483,13 +537,13 @@ module Prism if node.parent.nil? builder.const_global( token(node.delimiter_loc), - [node.child.name, srange(node.child.location)] + [node.name, srange(node.name_loc)] ) else builder.const_fetch( visit(node.parent), token(node.delimiter_loc), - [node.child.name, srange(node.child.location)] + [node.name, srange(node.name_loc)] ) end end @@ -512,18 +566,30 @@ module Prism def visit_constant_path_operator_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo::Bar &&= baz # ^^^^^^^^^^^^^^^^ - alias visit_constant_path_and_write_node visit_constant_path_operator_write_node + def visit_constant_path_and_write_node(node) + builder.op_assign( + builder.assignable(visit(node.target)), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # Foo::Bar ||= baz # ^^^^^^^^^^^^^^^^ - alias visit_constant_path_or_write_node visit_constant_path_operator_write_node + def visit_constant_path_or_write_node(node) + builder.op_assign( + builder.assignable(visit(node.target)), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # Foo::Bar, = baz # ^^^^^^^^ @@ -711,18 +777,30 @@ module Prism def visit_global_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # $foo &&= bar # ^^^^^^^^^^^^ - alias visit_global_variable_and_write_node visit_global_variable_operator_write_node + def visit_global_variable_and_write_node(node) + builder.op_assign( + builder.assignable(builder.gvar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # $foo ||= bar # ^^^^^^^^^^^^ - alias visit_global_variable_or_write_node visit_global_variable_operator_write_node + def visit_global_variable_or_write_node(node) + builder.op_assign( + builder.assignable(builder.gvar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # $foo, = bar # ^^^^ @@ -839,7 +917,7 @@ module Prism token(node.in_loc), pattern, guard, - srange_find(node.pattern.location.end_offset, node.statements&.location&.start_offset || node.location.end_offset, [";", "then"]), + srange_find(node.pattern.location.end_offset, node.statements&.location&.start_offset, [";", "then"]), visit(node.statements) ) end @@ -857,18 +935,46 @@ module Prism visit_all(arguments), token(node.closing_loc) ), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo[bar] &&= baz # ^^^^^^^^^^^^^^^^ - alias visit_index_and_write_node visit_index_operator_write_node + def visit_index_and_write_node(node) + arguments = node.arguments&.arguments || [] + arguments << node.block if node.block + + builder.op_assign( + builder.index( + visit(node.receiver), + token(node.opening_loc), + visit_all(arguments), + token(node.closing_loc) + ), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo[bar] ||= baz # ^^^^^^^^^^^^^^^^ - alias visit_index_or_write_node visit_index_operator_write_node + def visit_index_or_write_node(node) + arguments = node.arguments&.arguments || [] + arguments << node.block if node.block + + builder.op_assign( + builder.index( + visit(node.receiver), + token(node.opening_loc), + visit_all(arguments), + token(node.closing_loc) + ), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo[bar], = 1 # ^^^^^^^^ @@ -902,18 +1008,30 @@ module Prism def visit_instance_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @foo &&= bar # ^^^^^^^^^^^^ - alias visit_instance_variable_and_write_node visit_instance_variable_operator_write_node + def visit_instance_variable_and_write_node(node) + builder.op_assign( + builder.assignable(builder.ivar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # @foo ||= bar # ^^^^^^^^^^^^ - alias visit_instance_variable_or_write_node visit_instance_variable_operator_write_node + def visit_instance_variable_or_write_node(node) + builder.op_assign( + builder.assignable(builder.ivar(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # @foo, = bar # ^^^^ @@ -1030,6 +1148,12 @@ module Prism end end + # -> { it } + # ^^^^^^^^^ + def visit_it_parameters_node(node) + builder.args(nil, [], nil, false) + end + # foo(bar: baz) # ^^^^^^^^ def visit_keyword_hash_node(node) @@ -1052,13 +1176,14 @@ module Prism # ^^^^^ def visit_lambda_node(node) parameters = node.parameters + implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( builder.call_lambda(token(node.operator_loc)), [node.opening, srange(node.opening_loc)], if parameters.nil? builder.args(nil, [], nil, false) - elsif node.parameters.is_a?(NumberedParametersNode) + elsif implicit_parameters visit(node.parameters) else builder.args( @@ -1068,7 +1193,7 @@ module Prism false ) end, - node.body&.accept(copy_compiler(forwarding: parameters.is_a?(NumberedParametersNode) ? [] : find_forwarding(parameters&.parameters))), + node.body&.accept(copy_compiler(forwarding: implicit_parameters ? [] : find_forwarding(parameters&.parameters))), [node.closing, srange(node.closing_loc)] ) end @@ -1076,7 +1201,14 @@ module Prism # foo # ^^^ def visit_local_variable_read_node(node) - builder.ident([node.name, srange(node.location)]).updated(:lvar) + 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) end # foo = 1 @@ -1094,18 +1226,30 @@ module Prism def visit_local_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), - [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo &&= bar # ^^^^^^^^^^^ - alias visit_local_variable_and_write_node visit_local_variable_operator_write_node + def visit_local_variable_and_write_node(node) + builder.op_assign( + builder.assignable(builder.ident(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo ||= bar # ^^^^^^^^^^^ - alias visit_local_variable_or_write_node visit_local_variable_operator_write_node + def visit_local_variable_or_write_node(node) + builder.op_assign( + builder.assignable(builder.ident(token(node.name_loc))), + [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], + visit(node.value) + ) + end # foo, = bar # ^^^ @@ -1535,19 +1679,29 @@ module Prism elsif node.opening == "?" builder.character([node.unescaped, srange(node.location)]) else - parts = if node.content.lines.count <= 1 || node.unescaped.lines.count <= 1 - [builder.string_internal([node.unescaped, srange(node.content_loc)])] - else - start_offset = node.content_loc.start_offset + content_lines = node.content.lines + unescaped_lines = node.unescaped.lines - [node.content.lines, node.unescaped.lines].transpose.map do |content_line, unescaped_line| - end_offset = start_offset + content_line.length - offsets = srange_offsets(start_offset, end_offset) - start_offset = end_offset + parts = + if content_lines.length <= 1 || unescaped_lines.length <= 1 + [builder.string_internal([node.unescaped, srange(node.content_loc)])] + elsif content_lines.length != unescaped_lines.length + # This occurs when we have line continuations in the string. We + # need to come back and fix this, but for now this stops the + # code from breaking when we encounter it because of trying to + # transpose arrays of different lengths. + [builder.string_internal([node.unescaped, srange(node.content_loc)])] + else + start_offset = node.content_loc.start_offset - builder.string_internal([unescaped_line, offsets]) + [content_lines, unescaped_lines].transpose.map do |content_line, unescaped_line| + end_offset = start_offset + content_line.length + offsets = srange_offsets(start_offset, end_offset) + start_offset = end_offset + + builder.string_internal([unescaped_line, offsets]) + end end - end builder.string_compose( token(node.opening_loc), @@ -1655,7 +1809,7 @@ module Prism end # until foo; bar end - # ^^^^^^^^^^^^^^^^^ + # ^^^^^^^^^^^^^^^^^^ # # bar until foo # ^^^^^^^^^^^^^ @@ -1688,7 +1842,7 @@ module Prism if node.then_keyword_loc token(node.then_keyword_loc) else - srange_find(node.conditions.last.location.end_offset, node.statements&.location&.start_offset || (node.conditions.last.location.end_offset + 1), [";"]) + srange_find(node.conditions.last.location.end_offset, node.statements&.location&.start_offset, [";"]) end, visit(node.statements) ) @@ -1847,12 +2001,16 @@ module Prism # Constructs a new source range by finding the given tokens between the # given start offset and end offset. If the needle is not found, it - # returns nil. + # returns nil. Importantly it does not search past newlines or comments. + # + # 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) - tokens.find do |token| - next unless (index = source_buffer.source.byteslice(start_offset...end_offset).index(token)) - offset = start_offset + index - return [token, Range.new(source_buffer, offset_cache[offset], offset_cache[offset + token.length])] + if (match = source_buffer.source.byteslice(start_offset...end_offset).match(/(\s*)(#{tokens.join("|")})/)) + _, whitespace, token = *match + token_offset = start_offset + whitespace.bytesize + + [token, Range.new(source_buffer, offset_cache[token_offset], offset_cache[token_offset + token.bytesize])] end end @@ -1865,13 +2023,14 @@ module Prism def visit_block(call, block) if block parameters = block.parameters + implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( call, token(block.opening_loc), if parameters.nil? builder.args(nil, [], nil, false) - elsif parameters.is_a?(NumberedParametersNode) + elsif implicit_parameters visit(parameters) else builder.args( @@ -1886,7 +2045,7 @@ module Prism false ) end, - block.body&.accept(copy_compiler(forwarding: parameters.is_a?(NumberedParametersNode) ? [] : find_forwarding(parameters&.parameters))), + block.body&.accept(copy_compiler(forwarding: implicit_parameters ? [] : find_forwarding(parameters&.parameters))), token(block.closing_loc) ) else diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 3c06f6a40d..68f658565d 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -19,31 +19,31 @@ module Prism # The main known difference is that we may omit dispatching some events in # some cases. This impacts the following events: # - # * on_assign_error - # * on_comma - # * on_ignored_nl - # * on_ignored_sp - # * on_kw - # * on_label_end - # * on_lbrace - # * on_lbracket - # * on_lparen - # * on_nl - # * on_op - # * on_operator_ambiguous - # * on_rbrace - # * on_rbracket - # * on_rparen - # * on_semicolon - # * on_sp - # * on_symbeg - # * on_tstring_beg - # * on_tstring_end + # - on_assign_error + # - on_comma + # - on_ignored_nl + # - on_ignored_sp + # - on_kw + # - on_label_end + # - on_lbrace + # - on_lbracket + # - on_lparen + # - on_nl + # - on_op + # - on_operator_ambiguous + # - on_rbrace + # - on_rbracket + # - on_rparen + # - on_semicolon + # - on_sp + # - on_symbeg + # - on_tstring_beg + # - on_tstring_end # class Ripper < Compiler # Parses the given Ruby program read from +src+. # +src+ must be a String or an IO or a object with a #gets method. - def Ripper.parse(src, filename = "(ripper)", lineno = 1) + def self.parse(src, filename = "(ripper)", lineno = 1) new(src, filename, lineno).parse end @@ -54,22 +54,22 @@ module Prism # By default, this method does not handle syntax errors in +src+, # use the +raise_errors+ keyword to raise a SyntaxError for an error in +src+. # - # require 'ripper' - # require 'pp' + # require "ripper" + # require "pp" # - # pp Ripper.lex("def m(a) nil end") - # #=> [[[1, 0], :on_kw, "def", FNAME ], - # [[1, 3], :on_sp, " ", FNAME ], - # [[1, 4], :on_ident, "m", ENDFN ], - # [[1, 5], :on_lparen, "(", BEG|LABEL], - # [[1, 6], :on_ident, "a", ARG ], - # [[1, 7], :on_rparen, ")", ENDFN ], - # [[1, 8], :on_sp, " ", BEG ], - # [[1, 9], :on_kw, "nil", END ], - # [[1, 12], :on_sp, " ", END ], - # [[1, 13], :on_kw, "end", END ]] + # pp Ripper.lex("def m(a) nil end") + # #=> [[[1, 0], :on_kw, "def", FNAME ], + # [[1, 3], :on_sp, " ", FNAME ], + # [[1, 4], :on_ident, "m", ENDFN ], + # [[1, 5], :on_lparen, "(", BEG|LABEL], + # [[1, 6], :on_ident, "a", ARG ], + # [[1, 7], :on_rparen, ")", ENDFN ], + # [[1, 8], :on_sp, " ", BEG ], + # [[1, 9], :on_kw, "nil", END ], + # [[1, 12], :on_sp, " ", END ], + # [[1, 13], :on_kw, "end", END ]] # - def Ripper.lex(src, filename = "-", lineno = 1, raise_errors: false) + def self.lex(src, filename = "-", lineno = 1, raise_errors: false) result = Prism.lex_compat(src, filepath: filename, line: lineno) if result.failure? && raise_errors @@ -368,17 +368,17 @@ module Prism # returning +nil+ in such cases. Use the +raise_errors+ keyword # to raise a SyntaxError for an error in +src+. # - # require "ripper" - # require "pp" + # require "ripper" + # require "pp" # - # pp Ripper.sexp("def m(a) nil end") - # #=> [:program, - # [[:def, - # [:@ident, "m", [1, 4]], - # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil, nil, nil, nil]], - # [:bodystmt, [[:var_ref, [:@kw, "nil", [1, 9]]]], nil, nil, nil]]]] + # pp Ripper.sexp("def m(a) nil end") + # #=> [:program, + # [[:def, + # [:@ident, "m", [1, 4]], + # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil, nil, nil, nil]], + # [:bodystmt, [[:var_ref, [:@kw, "nil", [1, 9]]]], nil, nil, nil]]]] # - def Ripper.sexp(src, filename = "-", lineno = 1, raise_errors: false) + def self.sexp(src, filename = "-", lineno = 1, raise_errors: false) builder = SexpBuilderPP.new(src, filename, lineno) sexp = builder.parse if builder.error? @@ -397,23 +397,23 @@ module Prism # returning +nil+ in such cases. Use the +raise_errors+ keyword # to raise a SyntaxError for an error in +src+. # - # require 'ripper' - # require 'pp' + # require "ripper" + # require "pp" # - # pp Ripper.sexp_raw("def m(a) nil end") - # #=> [:program, - # [:stmts_add, - # [:stmts_new], - # [:def, - # [:@ident, "m", [1, 4]], - # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil]], - # [:bodystmt, - # [:stmts_add, [:stmts_new], [:var_ref, [:@kw, "nil", [1, 9]]]], - # nil, - # nil, - # nil]]]] + # pp Ripper.sexp_raw("def m(a) nil end") + # #=> [:program, + # [:stmts_add, + # [:stmts_new], + # [:def, + # [:@ident, "m", [1, 4]], + # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil]], + # [:bodystmt, + # [:stmts_add, [:stmts_new], [:var_ref, [:@kw, "nil", [1, 9]]]], + # nil, + # nil, + # nil]]]] # - def Ripper.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false) + def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false) builder = SexpBuilder.new(src, filename, lineno) sexp = builder.parse if builder.error? @@ -1181,8 +1181,8 @@ module Prism bounds(node.location) target = on_field(receiver, call_operator, message) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -1339,8 +1339,8 @@ module Prism bounds(node.name_loc) target = on_var_field(on_cvar(node.name.to_s)) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -1409,8 +1409,8 @@ module Prism bounds(node.name_loc) target = on_var_field(on_const(node.name.to_s)) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -1456,16 +1456,16 @@ module Prism # ^^^^^^^^ def visit_constant_path_node(node) if node.parent.nil? - bounds(node.child.location) - child = on_const(node.child.name.to_s) + bounds(node.name_loc) + child = on_const(node.name.to_s) bounds(node.location) on_top_const_ref(child) else parent = visit(node.parent) - bounds(node.child.location) - child = on_const(node.child.name.to_s) + bounds(node.name_loc) + child = on_const(node.name.to_s) bounds(node.location) on_const_path_ref(parent, child) @@ -1488,16 +1488,16 @@ module Prism # Visit a constant path that is part of a write node. private def visit_constant_path_write_node_target(node) if node.parent.nil? - bounds(node.child.location) - child = on_const(node.child.name.to_s) + bounds(node.name_loc) + child = on_const(node.name.to_s) bounds(node.location) on_top_const_field(child) else parent = visit(node.parent) - bounds(node.child.location) - child = on_const(node.child.name.to_s) + bounds(node.name_loc) + child = on_const(node.name.to_s) bounds(node.location) on_const_path_field(parent, child) @@ -1510,8 +1510,8 @@ module Prism target = visit_constant_path_write_node_target(node.target) value = visit(node.value) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -1802,8 +1802,8 @@ module Prism bounds(node.name_loc) target = on_var_field(on_gvar(node.name.to_s)) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -1983,8 +1983,8 @@ module Prism bounds(node.location) target = on_aref_field(receiver, arguments) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -2059,8 +2059,8 @@ module Prism bounds(node.name_loc) target = on_var_field(on_ivar(node.name.to_s)) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -2337,8 +2337,8 @@ module Prism bounds(node.name_loc) target = on_var_field(on_ident(node.name_loc.slice)) - bounds(node.operator_loc) - operator = on_op("#{node.operator}=") + bounds(node.binary_operator_loc) + operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) @@ -3267,7 +3267,11 @@ module Prism # Lazily initialize the parse result. def result - @result ||= Prism.parse(source) + @result ||= + begin + scopes = RUBY_VERSION >= "3.3.0" ? [] : [[]] + Prism.parse(source, scopes: scopes) + end end ########################################################################## diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 5c59fe3181..43aac23be7 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 @@ -271,9 +276,9 @@ module Prism # ^^^^^^^^^^^^^^^ def visit_call_operator_write_node(node) if op_asgn?(node) - s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, node.operator) + s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, node.binary_operator) else - s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, node.operator, visit_write_value(node.value)) + s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, node.binary_operator, visit_write_value(node.value)) end end @@ -372,7 +377,7 @@ module Prism # @@foo += bar # ^^^^^^^^^^^^ def visit_class_variable_operator_write_node(node) - s(node, class_variable_write_type, node.name, s(node, :call, s(node, :cvar, node.name), node.operator, visit_write_value(node.value))) + s(node, class_variable_write_type, node.name, s(node, :call, s(node, :cvar, node.name), node.binary_operator, visit_write_value(node.value))) end # @@foo &&= bar @@ -417,7 +422,7 @@ module Prism # Foo += bar # ^^^^^^^^^^^ def visit_constant_operator_write_node(node) - s(node, :cdecl, node.name, s(node, :call, s(node, :const, node.name), node.operator, visit_write_value(node.value))) + s(node, :cdecl, node.name, s(node, :call, s(node, :const, node.name), node.binary_operator, visit_write_value(node.value))) end # Foo &&= bar @@ -442,9 +447,9 @@ module Prism # ^^^^^^^^ def visit_constant_path_node(node) 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 end @@ -460,7 +465,7 @@ module Prism # Foo::Bar += baz # ^^^^^^^^^^^^^^^ def visit_constant_path_operator_write_node(node) - s(node, :op_asgn, visit(node.target), node.operator, visit_write_value(node.value)) + s(node, :op_asgn, visit(node.target), node.binary_operator, visit_write_value(node.value)) end # Foo::Bar &&= baz @@ -627,7 +632,7 @@ module Prism # $foo += bar # ^^^^^^^^^^^ def visit_global_variable_operator_write_node(node) - s(node, :gasgn, node.name, s(node, :call, s(node, :gvar, node.name), node.operator, visit(node.value))) + s(node, :gasgn, node.name, s(node, :call, s(node, :gvar, node.name), node.binary_operator, visit(node.value))) end # $foo &&= bar @@ -719,7 +724,7 @@ module Prism arglist << visit(node.block) if !node.block.nil? end - s(node, :op_asgn1, visit(node.receiver), arglist, node.operator, visit_write_value(node.value)) + s(node, :op_asgn1, visit(node.receiver), arglist, node.binary_operator, visit_write_value(node.value)) end # foo[bar] &&= baz @@ -775,7 +780,7 @@ module Prism # @foo += bar # ^^^^^^^^^^^ def visit_instance_variable_operator_write_node(node) - s(node, :iasgn, node.name, s(node, :call, s(node, :ivar, node.name), node.operator, visit_write_value(node.value))) + s(node, :iasgn, node.name, s(node, :call, s(node, :ivar, node.name), node.binary_operator, visit_write_value(node.value))) end # @foo &&= bar @@ -805,17 +810,29 @@ module Prism # if /foo #{bar}/ then end # ^^^^^^^^^^^^ def visit_interpolated_match_last_line_node(node) - s(node, :match, s(node, :dregx).concat(visit_interpolated_parts(node.parts))) + parts = visit_interpolated_parts(node.parts) + regexp = + if parts.length == 1 + s(node, :lit, Regexp.new(parts.first, node.options)) + else + s(node, :dregx).concat(parts).tap do |result| + options = node.options + result << options if options != 0 + end + end + + s(node, :match, regexp) end # /foo #{bar}/ # ^^^^^^^^^^^^ def visit_interpolated_regular_expression_node(node) - if node.parts.all? { |part| part.is_a?(StringNode) || (part.is_a?(EmbeddedStatementsNode) && part.statements&.body&.length == 1 && part.statements.body.first.is_a?(StringNode)) } - unescaped = node.parts.map { |part| part.is_a?(StringNode) ? part.unescaped : part.statements.body.first.unescaped }.join - s(node, :lit, Regexp.new(unescaped, node.options)) + parts = visit_interpolated_parts(node.parts) + + if parts.length == 1 + s(node, :lit, Regexp.new(parts.first, node.options)) else - s(node, :dregx).concat(visit_interpolated_parts(node.parts)).tap do |result| + s(node, :dregx).concat(parts).tap do |result| options = node.options result << options if options != 0 end @@ -825,45 +842,71 @@ module Prism # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) - if (node.parts.all? { |part| part.is_a?(StringNode) || (part.is_a?(EmbeddedStatementsNode) && part.statements&.body&.length == 1 && part.statements.body.first.is_a?(StringNode)) }) || - (node.opening.nil? && node.parts.all? { |part| part.is_a?(StringNode) && !part.opening_loc.nil? }) - unescaped = node.parts.map { |part| part.is_a?(StringNode) ? part.unescaped : part.statements.body.first.unescaped }.join - s(node, :str, unescaped) - else - s(node, :dstr).concat(visit_interpolated_parts(node.parts)) - end + parts = visit_interpolated_parts(node.parts) + parts.length == 1 ? s(node, :str, parts.first) : s(node, :dstr).concat(parts) end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) - if node.parts.all? { |part| part.is_a?(StringNode) || (part.is_a?(EmbeddedStatementsNode) && part.statements&.body&.length == 1 && part.statements.body.first.is_a?(StringNode)) } - unescaped = node.parts.map { |part| part.is_a?(StringNode) ? part.unescaped : part.statements.body.first.unescaped }.join - s(node, :lit, unescaped.to_sym) - else - s(node, :dsym).concat(visit_interpolated_parts(node.parts)) - end + parts = visit_interpolated_parts(node.parts) + parts.length == 1 ? s(node, :lit, parts.first.to_sym) : s(node, :dsym).concat(parts) end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) - children = visit_interpolated_parts(node.parts) - s(node.heredoc? ? node.parts.first : node, :dxstr).concat(children) + source = node.heredoc? ? node.parts.first : node + parts = visit_interpolated_parts(node.parts) + parts.length == 1 ? s(source, :xstr, parts.first) : s(source, :dxstr).concat(parts) end # Visit the interpolated content of the string-like node. private def visit_interpolated_parts(parts) - parts.each_with_object([]).with_index do |(part, results), index| - if index == 0 - if part.is_a?(StringNode) - results << part.unescaped + visited = [] + parts.each do |part| + result = visit(part) + + if result[0] == :evstr && result[1] + if result[1][0] == :str + visited << result[1] + elsif result[1][0] == :dstr + visited.concat(result[1][1..-1]) + else + visited << result + end + else + visited << result + end + end + + state = :beginning #: :beginning | :string_content | :interpolated_content + + visited.each_with_object([]) do |result, results| + case state + when :beginning + if result.is_a?(String) + results << result + state = :string_content + elsif result.is_a?(Array) && result[0] == :str + results << result[1] + state = :string_content else results << "" - results << visit(part) + results << result + state = :interpolated_content + end + when :string_content + if result.is_a?(String) + results[0] << result + elsif result.is_a?(Array) && result[0] == :str + results[0] << result[1] + else + results << result + state = :interpolated_content end else - results << visit(part) + results << result end end end @@ -922,7 +965,7 @@ module Prism # foo += bar # ^^^^^^^^^^ def visit_local_variable_operator_write_node(node) - s(node, :lasgn, node.name, s(node, :call, s(node, :lvar, node.name), node.operator, visit_write_value(node.value))) + s(node, :lasgn, node.name, s(node, :call, s(node, :lvar, node.name), node.binary_operator, visit_write_value(node.value))) end # foo &&= bar @@ -1297,7 +1340,7 @@ module Prism # __FILE__ # ^^^^^^^^ def visit_source_file_node(node) - s(node, :str, file) + s(node, :str, node.filepath) end # __LINE__ @@ -1498,13 +1541,13 @@ module Prism # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse(source, filepath = "(string)") - translate(Prism.parse(source), filepath) + translate(Prism.parse(source, filepath: filepath, scopes: [[]]), filepath) end # Parse the given file and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse_file(filepath) - translate(Prism.parse_file(filepath), filepath) + translate(Prism.parse_file(filepath, scopes: [[]]), filepath) end class << self diff --git a/lib/reline.rb b/lib/reline.rb index f0060f5c9c..fb00b96531 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -225,17 +225,20 @@ module Reline journey_data = completion_journey_data return unless journey_data - target = journey_data.list[journey_data.pointer] + target = journey_data.list.first + completed = journey_data.list[journey_data.pointer] result = journey_data.list.drop(1) pointer = journey_data.pointer - 1 - return if target.empty? || (result == [target] && pointer < 0) + return if completed.empty? || (result == [completed] && pointer < 0) target_width = Reline::Unicode.calculate_width(target) - x = cursor_pos.x - target_width - if x < 0 - x = screen_width + x + completed_width = Reline::Unicode.calculate_width(completed) + if cursor_pos.x <= completed_width - target_width + # When target is rendered on the line above cursor position + x = screen_width - completed_width y = -1 else + x = [cursor_pos.x - completed_width, 0].max y = 0 end cursor_pos_to_render = Reline::CursorPos.new(x, y) @@ -309,6 +312,10 @@ module Reline $stderr.sync = true $stderr.puts "Reline is used by #{Process.pid}" end + unless config.test_mode or config.loaded? + config.read + io_gate.set_default_key_bindings(config) + end otio = io_gate.prep may_req_ambiguous_char_width @@ -335,12 +342,6 @@ module Reline end end - unless config.test_mode - config.read - config.reset_default_key_bindings - io_gate.set_default_key_bindings(config) - end - line_editor.print_nomultiline_prompt(prompt) line_editor.update_dialogs line_editor.rerender @@ -350,7 +351,15 @@ module Reline loop do read_io(config.keyseq_timeout) { |inputs| line_editor.set_pasting_state(io_gate.in_pasting?) - inputs.each { |key| line_editor.update(key) } + inputs.each do |key| + if key.char == :bracketed_paste_start + text = io_gate.read_bracketed_paste + line_editor.insert_pasted_text(text) + line_editor.scroll_into_view + else + line_editor.update(key) + end + end } if line_editor.finished? line_editor.render_finished diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb index a3719f502c..45a475a787 100644 --- a/lib/reline/ansi.rb +++ b/lib/reline/ansi.rb @@ -45,6 +45,7 @@ class Reline::ANSI end def self.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? set_default_key_bindings_terminfo(config) @@ -66,6 +67,12 @@ class Reline::ANSI end end + def self.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) ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| bindings = [["\e[#{char}", default_func]] # CSI + char @@ -178,46 +185,26 @@ class Reline::ANSI nil end - @@in_bracketed_paste_mode = false - START_BRACKETED_PASTE = String.new("\e[200~,", encoding: Encoding::ASCII_8BIT) - END_BRACKETED_PASTE = String.new("\e[200~.", encoding: Encoding::ASCII_8BIT) - def self.getc_with_bracketed_paste(timeout_second) + 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 buffer = String.new(encoding: Encoding::ASCII_8BIT) - buffer << inner_getc(timeout_second) - while START_BRACKETED_PASTE.start_with?(buffer) or END_BRACKETED_PASTE.start_with?(buffer) do - if START_BRACKETED_PASTE == buffer - @@in_bracketed_paste_mode = true - return inner_getc(timeout_second) - elsif END_BRACKETED_PASTE == buffer - @@in_bracketed_paste_mode = false - ungetc(-1) - return inner_getc(timeout_second) - end - succ_c = inner_getc(Reline.core.config.keyseq_timeout) - - if succ_c - buffer << succ_c - else - break - end + until buffer.end_with?(END_BRACKETED_PASTE) + c = inner_getc(Float::INFINITY) + break unless c + buffer << c end - buffer.bytes.reverse_each do |ch| - ungetc ch - end - inner_getc(timeout_second) + string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding) + string.valid_encoding? ? string : '' end # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second def self.getc(timeout_second) - if Reline.core.config.enable_bracketed_paste - getc_with_bracketed_paste(timeout_second) - else - inner_getc(timeout_second) - end + inner_getc(timeout_second) end def self.in_pasting? - @@in_bracketed_paste_mode or (not empty_buffer?) + not empty_buffer? end def self.empty_buffer? @@ -284,7 +271,7 @@ class Reline::ANSI buf = @@output.pread(@@output.pos, 0) row = buf.count("\n") column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 - rescue Errno::ESPIPE + rescue Errno::ESPIPE, IOError # Just returns column 1 for ambiguous width because this I/O is not # tty and can't seek. row = 0 @@ -361,11 +348,15 @@ class Reline::ANSI end def self.prep + # Enable bracketed paste + @@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste retrieve_keybuffer nil end def self.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 end end diff --git a/lib/reline/config.rb b/lib/reline/config.rb index 4c07a73701..d44c2675ab 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -8,31 +8,12 @@ class Reline::Config end VARIABLE_NAMES = %w{ - bind-tty-special-chars - blink-matching-paren - byte-oriented completion-ignore-case convert-meta disable-completion - enable-keypad - expand-tilde - history-preserve-point history-size - horizontal-scroll-mode - input-meta keyseq-timeout - mark-directories - mark-modified-lines - mark-symlinked-directories - match-hidden-files - meta-flag - output-meta - page-completions - prefer-visible-bell - print-completions-horizontally show-all-if-ambiguous - show-all-if-unmodified - visible-stats show-mode-in-prompt vi-cmd-mode-string vi-ins-mode-string @@ -53,8 +34,6 @@ class Reline::Config @additional_key_bindings[:vi_insert] = {} @additional_key_bindings[:vi_command] = {} @oneshot_key_bindings = {} - @skip_section = nil - @if_stack = nil @editing_mode_label = :emacs @keymap_label = :emacs @keymap_prefix = [] @@ -71,17 +50,15 @@ class Reline::Config @test_mode = false @autocompletion = false @convert_meta = true if 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 - @additional_key_bindings.keys.each do |key| - @additional_key_bindings[key].clear - end @oneshot_key_bindings.clear - reset_default_key_bindings end def editing_mode @@ -100,6 +77,10 @@ class Reline::Config @key_actors[@keymap_label] end + def loaded? + @loaded + end + def inputrc_path case ENV['INPUTRC'] when nil, '' @@ -131,6 +112,7 @@ class Reline::Config end def read(file = nil) + @loaded = true file ||= default_inputrc_path begin if file.respond_to?(:readlines) @@ -173,12 +155,6 @@ class Reline::Config @key_actors[@keymap_label].default_key_bindings[keystroke] = target end - def reset_default_key_bindings - @key_actors.values.each do |ka| - ka.reset_default_key_bindings - end - end - def read_lines(lines, file = nil) if not lines.empty? and lines.first.encoding != Reline.encoding_system_needs begin @@ -190,9 +166,7 @@ class Reline::Config end end end - conditions = [@skip_section, @if_stack] - @skip_section = nil - @if_stack = [] + if_stack = [] lines.each_with_index do |line, no| next if line.match(/\A\s*#/) @@ -201,11 +175,11 @@ class Reline::Config line = line.chomp.lstrip if line.start_with?('$') - handle_directive(line[1..-1], file, no) + handle_directive(line[1..-1], file, no, if_stack) next end - next if @skip_section + next if if_stack.any? { |_no, skip| skip } case line when /^set +([^ ]+) +([^ ]+)/i @@ -214,43 +188,47 @@ class Reline::Config 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 end end - unless @if_stack.empty? - raise InvalidInputrc, "#{file}:#{@if_stack.last[1]}: unclosed if" + unless if_stack.empty? + raise InvalidInputrc, "#{file}:#{if_stack.last[0]}: unclosed if" end - ensure - @skip_section, @if_stack = conditions end - def handle_directive(directive, file, no) + def handle_directive(directive, file, no, if_stack) directive, args = directive.split(' ') case directive when 'if' condition = false case args - when 'mode' + when /^mode=(vi|emacs)$/i + mode = $1.downcase + # NOTE: mode=vi means vi-insert mode + mode = 'vi_insert' if mode == 'vi' + if @editing_mode_label == mode.to_sym + condition = true + end when 'term' when 'version' else # application name condition = true if args == 'Ruby' condition = true if args == 'Reline' end - @if_stack << [file, no, @skip_section] - @skip_section = !condition + if_stack << [no, !condition] when 'else' - if @if_stack.empty? + if if_stack.empty? raise InvalidInputrc, "#{file}:#{no}: unmatched else" end - @skip_section = !@skip_section + if_stack.last[1] = !if_stack.last[1] when 'endif' - if @if_stack.empty? + if if_stack.empty? raise InvalidInputrc, "#{file}:#{no}: unmatched endif" end - @skip_section = @if_stack.pop + if_stack.pop when 'include' read(File.expand_path(args)) end diff --git a/lib/reline/key_actor/base.rb b/lib/reline/key_actor/base.rb index a1cd7fb2a1..194e98938c 100644 --- a/lib/reline/key_actor/base.rb +++ b/lib/reline/key_actor/base.rb @@ -12,8 +12,4 @@ class Reline::KeyActor::Base def default_key_bindings @default_key_bindings end - - def reset_default_key_bindings - @default_key_bindings.clear - end end diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb index 5d0a7fb63d..edd88289a3 100644 --- a/lib/reline/key_actor/emacs.rb +++ b/lib/reline/key_actor/emacs.rb @@ -19,7 +19,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 8 ^H :em_delete_prev_char, # 9 ^I - :ed_unassigned, + :complete, # 10 ^J :ed_newline, # 11 ^K @@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base # 30 ^^ :ed_unassigned, # 31 ^_ - :ed_unassigned, + :undo, # 32 SPACE :ed_insert, # 33 ! diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb index c3d7f9c12d..f8ccf468c6 100644 --- a/lib/reline/key_actor/vi_insert.rb +++ b/lib/reline/key_actor/vi_insert.rb @@ -19,7 +19,7 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base # 8 ^H :vi_delete_prev_char, # 9 ^I - :ed_insert, + :complete, # 10 ^J :ed_newline, # 11 ^K @@ -29,11 +29,11 @@ class Reline::KeyActor::ViInsert < Reline::KeyActor::Base # 13 ^M :ed_newline, # 14 ^N - :ed_insert, + :menu_complete, # 15 ^O :ed_insert, # 16 ^P - :ed_insert, + :menu_complete_backward, # 17 ^Q :ed_ignore, # 18 ^R diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index b2a963c6ab..23ece60220 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 @@ -75,7 +74,7 @@ class Reline::LineEditor def initialize(config, encoding) @config = config @completion_append_character = '' - @screen_size = Reline::IOGate.get_screen_size + @screen_size = [0, 0] # Should be initialized with actual winsize in LineEditor#reset reset_variables(encoding: encoding) end @@ -235,7 +234,6 @@ class Reline::LineEditor @vi_waiting_operator_arg = nil @completion_journey_state = nil @completion_state = CompletionState::NORMAL - @completion_occurs = false @perfect_matched = nil @menu_info = nil @searching_prompt = nil @@ -252,6 +250,8 @@ class Reline::LineEditor @resized = false @cache = {} @rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0) + @past_lines = [] + @undoing = false reset_line end @@ -284,7 +284,7 @@ class Reline::LineEditor indent1 = @auto_indent_proc.(@buffer_of_lines.take(@line_index - 1).push(''), @line_index - 1, 0, true) indent2 = @auto_indent_proc.(@buffer_of_lines.take(@line_index), @line_index - 1, @buffer_of_lines[@line_index - 1].bytesize, false) indent = indent2 || indent1 - @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A */, '') + @buffer_of_lines[@line_index - 1] = ' ' * indent + @buffer_of_lines[@line_index - 1].gsub(/\A\s*/, '') ) process_auto_indent @line_index, add_newline: true else @@ -295,8 +295,8 @@ class Reline::LineEditor end end - private def split_by_width(str, max_width) - Reline::Unicode.split_by_width(str, max_width, @encoding) + private def split_by_width(str, max_width, offset: 0) + Reline::Unicode.split_by_width(str, max_width, @encoding, offset: offset) end def current_byte_pointer_cursor @@ -370,7 +370,7 @@ class Reline::LineEditor @scroll_partial_screen end - def wrapped_lines + def wrapped_prompt_and_input_lines with_cache(__method__, @buffer_of_lines.size, modified_lines, prompt_list, screen_width) do |n, lines, prompts, width, prev_cache_key, cached_value| prev_n, prev_lines, prev_prompts, prev_width = prev_cache_key cached_wraps = {} @@ -381,9 +381,14 @@ class Reline::LineEditor end n.times.map do |i| - prompt = prompts[i] - line = lines[i] - cached_wraps[[prompt, line]] || split_by_width("#{prompt}#{line}", width).first.compact + prompt = prompts[i] || '' + line = lines[i] || '' + if (cached = cached_wraps[[prompt, line]]) + next cached + end + *wrapped_prompts, code_line_prompt = split_by_width(prompt, width).first.compact + wrapped_lines = split_by_width(line, width, offset: calculate_width(code_line_prompt, true)).first.compact + wrapped_prompts.map { |p| [p, ''] } + [[code_line_prompt, wrapped_lines.first]] + wrapped_lines.drop(1).map { |c| ['', c] } end end end @@ -409,8 +414,13 @@ class Reline::LineEditor @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" else x, w, content = new_items[level] - content = Reline::Unicode.take_range(content, base_x - x, width) unless x == base_x && w == width - Reline::IOGate.move_cursor_column base_x + cover_begin = base_x != 0 && new_levels[base_x - 1] == level + cover_end = new_levels[base_x + width] == level + pos = 0 + unless x == base_x && w == width + 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}" end base_x += width @@ -426,7 +436,7 @@ class Reline::LineEditor prompt_width = calculate_width(prompt_list[@line_index], true) line_before_cursor = whole_lines[@line_index].byteslice(0, @byte_pointer) wrapped_line_before_cursor = split_by_width(' ' * prompt_width + line_before_cursor, screen_width).first.compact - wrapped_cursor_y = wrapped_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1 + wrapped_cursor_y = wrapped_prompt_and_input_lines[0...@line_index].sum(&:size) + wrapped_line_before_cursor.size - 1 wrapped_cursor_x = calculate_width(wrapped_line_before_cursor.last) [wrapped_cursor_x, wrapped_cursor_y] end @@ -490,8 +500,9 @@ class Reline::LineEditor wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position rendered_lines = @rendered_screen.lines - new_lines = wrapped_lines.flatten[screen_scroll_top, screen_height].map do |l| - [[0, Reline::Unicode.calculate_width(l, true), l]] + 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]] end if @menu_info @menu_info.lines(screen_width).each do |item| @@ -507,7 +518,8 @@ class Reline::LineEditor y_range.each do |row| next if row < 0 || row >= screen_height dialog_rows = new_lines[row] ||= [] - dialog_rows[index + 1] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]] + # 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 @@ -692,13 +704,6 @@ class Reline::LineEditor DIALOG_DEFAULT_HEIGHT = 20 - private def padding_space_with_escape_sequences(str, width) - padding_width = width - calculate_width(str, true) - # padding_width should be only positive value. But macOS and Alacritty returns negative value. - padding_width = 0 if padding_width < 0 - str + (' ' * padding_width) - end - private def dialog_range(dialog, dialog_y) x_range = dialog.column...dialog.column + dialog.width y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size @@ -771,7 +776,7 @@ class Reline::LineEditor dialog.contents = contents.map.with_index do |item, i| line_sgr = i == pointer ? enhanced_sgr : default_sgr str_width = dialog.width - (scrollbar_pos.nil? ? 0 : @block_elem_width) - str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width) + str, = Reline::Unicode.take_mbchar_range(item, 0, str_width, padding: true) colored_content = "#{line_sgr}#{str}" if scrollbar_pos if scrollbar_pos <= (i * 2) and (i * 2 + 1) < (scrollbar_pos + bar_height) @@ -851,7 +856,7 @@ class Reline::LineEditor [target, preposing, completed, postposing] end - private def complete(list, just_show_list) + private def perform_completion(list, just_show_list) case @completion_state when CompletionState::NORMAL @completion_state = CompletionState::COMPLETION @@ -880,10 +885,12 @@ class Reline::LineEditor @completion_state = CompletionState::PERFECT_MATCH else @completion_state = CompletionState::MENU_WITH_PERFECT_MATCH + perform_completion(list, true) if @config.show_all_if_ambiguous end @perfect_matched = completed else @completion_state = CompletionState::MENU + perform_completion(list, true) if @config.show_all_if_ambiguous end if not just_show_list and target < completed @buffer_of_lines[@line_index] = (preposing + completed + completion_append_character.to_s + postposing).split("\n")[@line_index] || String.new(encoding: @encoding) @@ -942,7 +949,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 @@ -1003,7 +1011,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 @@ -1058,10 +1067,6 @@ class Reline::LineEditor end private def normal_char(key) - if key.combined_char.is_a?(Symbol) - process_key(key.combined_char, key.combined_char) - return - end @multibyte_buffer << key.combined_char if @multibyte_buffer.size > 1 if @multibyte_buffer.dup.force_encoding(@encoding).valid_encoding? @@ -1104,6 +1109,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 @@ -1111,38 +1117,17 @@ class Reline::LineEditor end end if key.char.nil? + process_insert(force: true) if @first_char @eof = true end finish return end - old_lines = @buffer_of_lines.dup @first_char = false @completion_occurs = false - if @config.editing_mode_is?(:emacs, :vi_insert) and key.char == "\C-i".ord - if !@config.disable_completion - process_insert(force: true) - if @config.autocompletion - @completion_state = CompletionState::NORMAL - @completion_occurs = move_completed_list(:down) - else - @completion_journey_state = nil - result = call_completion_proc - if result.is_a?(Array) - @completion_occurs = true - complete(result, false) - end - end - end - elsif @config.editing_mode_is?(:vi_insert) and ["\C-p".ord, "\C-n".ord].include?(key.char) - # In vi mode, move completed list even if autocompletion is off - if not @config.disable_completion - process_insert(force: true) - @completion_state = CompletionState::NORMAL - @completion_occurs = move_completed_list("\C-p".ord == key.char ? :up : :down) - end - elsif Symbol === key.char and respond_to?(key.char, true) + + if key.char.is_a?(Symbol) process_key(key.char, key.char) else normal_char(key) @@ -1152,12 +1137,15 @@ class Reline::LineEditor @completion_journey_state = nil end + push_past_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) @@ -1166,6 +1154,26 @@ class Reline::LineEditor modified end + def save_old_buffer + @old_buffer_of_lines = @buffer_of_lines.dup + @old_byte_pointer = @byte_pointer.dup + @old_line_index = @line_index.dup + end + + def push_past_lines + if @old_buffer_of_lines != @buffer_of_lines + @past_lines.push([@old_buffer_of_lines, @old_byte_pointer, @old_line_index]) + end + trim_past_lines + end + + MAX_PAST_LINES = 100 + def trim_past_lines + if @past_lines.size > MAX_PAST_LINES + @past_lines.shift + end + end + def scroll_into_view _wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position if wrapped_cursor_y < screen_scroll_top @@ -1242,6 +1250,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 @@ -1323,6 +1343,18 @@ class Reline::LineEditor @confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n") end + def insert_pasted_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) + lines << '' if lines.empty? + @buffer_of_lines[@line_index, 1] = lines + @line_index += lines.size - 1 + @byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize + push_past_lines + end + def insert_text(text) if @buffer_of_lines[@line_index].bytesize == @byte_pointer @buffer_of_lines[@line_index] += text @@ -1421,13 +1453,42 @@ class Reline::LineEditor end end - private def completion_journey_up(key) - if not @config.disable_completion and @config.autocompletion + private def complete(_key) + return if @config.disable_completion + + process_insert(force: true) + if @config.autocompletion @completion_state = CompletionState::NORMAL - @completion_occurs = move_completed_list(:up) + @completion_occurs = move_completed_list(:down) + else + @completion_journey_state = nil + result = call_completion_proc + if result.is_a?(Array) + @completion_occurs = true + perform_completion(result, false) + end end end - alias_method :menu_complete_backward, :completion_journey_up + + private def completion_journey_move(direction) + return if @config.disable_completion + + process_insert(force: true) + @completion_state = CompletionState::NORMAL + @completion_occurs = move_completed_list(direction) + end + + private def menu_complete(_key) + completion_journey_move(:down) + end + + private def menu_complete_backward(_key) + completion_journey_move(:up) + end + + private def completion_journey_up(_key) + completion_journey_move(:up) if @config.autocompletion + end # Editline:: +ed-unassigned+ This editor command always results in an error. # GNU Readline:: There is no corresponding macro. @@ -1533,11 +1594,7 @@ class Reline::LineEditor alias_method :vi_zero, :ed_move_to_beg private def ed_move_to_end(key) - @byte_pointer = 0 - while @byte_pointer < current_line.bytesize - byte_size = Reline::Unicode.get_next_mbchar_size(current_line, @byte_pointer) - @byte_pointer += byte_size - end + @byte_pointer = current_line.bytesize end alias_method :end_of_line, :ed_move_to_end @@ -1900,7 +1957,7 @@ class Reline::LineEditor elsif !@config.autocompletion # show completed list result = call_completion_proc if result.is_a?(Array) - complete(result, true) + perform_completion(result, true) end end end @@ -2470,4 +2527,15 @@ class Reline::LineEditor private def vi_editing_mode(key) @config.editing_mode = :vi_insert end + + private def undo(_key) + return if @past_lines.empty? + + @undoing = true + + target_lines, target_cursor_x, target_cursor_y = @past_lines.last + set_current_lines(target_lines, target_cursor_x, target_cursor_y) + + @past_lines.pop + end end diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb index 26ef207ba6..d7460d6d4a 100644 --- a/lib/reline/unicode.rb +++ b/lib/reline/unicode.rb @@ -43,11 +43,13 @@ class Reline::Unicode def self.escape_for_print(str) str.chars.map! { |gr| - escaped = EscapedPairs[gr.ord] - if escaped && gr != -"\n" && gr != -"\t" - escaped - else + case gr + when -"\n" gr + when -"\t" + -' ' + else + EscapedPairs[gr.ord] || gr end }.join end @@ -128,10 +130,10 @@ class Reline::Unicode end end - def self.split_by_width(str, max_width, encoding = str.encoding) + def self.split_by_width(str, max_width, encoding = str.encoding, offset: 0) lines = [String.new(encoding: encoding)] height = 1 - width = 0 + width = offset rest = str.encode(Encoding::UTF_8) in_zero_width = false seq = String.new(encoding: encoding) @@ -145,7 +147,13 @@ class Reline::Unicode lines.last << NON_PRINTING_END when csi lines.last << csi - seq << csi + unless in_zero_width + if csi == -"\e[m" || csi == -"\e[0m" + seq.clear + else + seq << csi + end + end when osc lines.last << osc seq << osc @@ -173,32 +181,78 @@ class Reline::Unicode # Take a chunk of a String cut by width with escape sequences. def self.take_range(str, start_col, max_width) + take_mbchar_range(str, start_col, max_width).first + end + + def self.take_mbchar_range(str, start_col, width, cover_begin: false, cover_end: false, padding: false) chunk = String.new(encoding: str.encoding) + + end_col = start_col + width total_width = 0 rest = str.encode(Encoding::UTF_8) in_zero_width = false + chunk_start_col = nil + chunk_end_col = nil + has_csi = false rest.scan(WIDTH_SCANNER) do |non_printing_start, non_printing_end, csi, osc, gc| case when non_printing_start in_zero_width = true + chunk << NON_PRINTING_START when non_printing_end in_zero_width = false + chunk << NON_PRINTING_END when csi + has_csi = true chunk << csi when osc chunk << osc when gc if in_zero_width chunk << gc + next + end + + mbchar_width = get_mbchar_width(gc) + prev_width = total_width + total_width += mbchar_width + + if (cover_begin || padding ? total_width <= start_col : prev_width < start_col) + # Current character haven't reached start_col yet + next + elsif padding && !cover_begin && prev_width < start_col && start_col < total_width + # Add preceding padding. This padding might have background color. + chunk << ' ' + chunk_start_col ||= start_col + chunk_end_col = total_width + next + elsif (cover_end ? prev_width < end_col : total_width <= end_col) + # Current character is in the range + chunk << gc + chunk_start_col ||= prev_width + chunk_end_col = total_width + break if total_width >= end_col else - mbchar_width = get_mbchar_width(gc) - total_width += mbchar_width - break if (start_col + max_width) < total_width - chunk << gc if start_col < total_width + # Current character exceeds end_col + if padding && end_col < total_width + # Add succeeding padding. This padding might have background color. + chunk << ' ' + chunk_start_col ||= prev_width + chunk_end_col = end_col + end + break end end end - chunk + chunk_start_col ||= start_col + chunk_end_col ||= start_col + if padding && chunk_end_col < end_col + # Append padding. This padding should not include background color. + chunk << "\e[0m" if has_csi + chunk << ' ' * (end_col - chunk_end_col) + chunk_end_col = end_col + end + [chunk, chunk_start_col, chunk_end_col - chunk_start_col] end def self.get_next_mbchar_size(line, byte_pointer) diff --git a/lib/reline/version.rb b/lib/reline/version.rb index e6f370c6ec..46613a5952 100644 --- a/lib/reline/version.rb +++ b/lib/reline/version.rb @@ -1,3 +1,3 @@ module Reline - VERSION = '0.5.2' + VERSION = '0.5.7' end diff --git a/lib/ruby_vm/rjit/insn_compiler.rb b/lib/ruby_vm/rjit/insn_compiler.rb index 2346c92bd1..f9450241c9 100644 --- a/lib/ruby_vm/rjit/insn_compiler.rb +++ b/lib/ruby_vm/rjit/insn_compiler.rb @@ -5461,6 +5461,12 @@ module RubyVM::RJIT return CantCompile end + if c_method_tracing_currently_enabled? + # Don't JIT if tracing c_call or c_return + asm.incr_counter(:send_cfunc_tracing) + return CantCompile + end + off = cme.def.body.optimized.index recv_idx = argc # blockarg is not supported diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index 3d6fecaa40..8e80d46856 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -197,18 +197,17 @@ command to remove old versions. yield else require "tmpdir" - tmpdir = Dir.mktmpdir - FileUtils.mv Gem.plugindir, tmpdir + Dir.mktmpdir("gem_update") do |tmpdir| + FileUtils.mv Gem.plugindir, tmpdir - status = yield + status = yield - if status - FileUtils.rm_rf tmpdir - else - FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir - end + unless status + FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir + end - status + status + end end end diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 58a6c5b7dc..7d24f9cbfc 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -69,99 +69,101 @@ # end # end -module Gem::Deprecate - def self.skip # :nodoc: - @skip ||= false - end +module Gem + module Deprecate + def self.skip # :nodoc: + @skip ||= false + end - def self.skip=(v) # :nodoc: - @skip = v - end + def self.skip=(v) # :nodoc: + @skip = v + end - ## - # Temporarily turn off warnings. Intended for tests only. + ## + # Temporarily turn off warnings. Intended for tests only. - def skip_during - original = Gem::Deprecate.skip - Gem::Deprecate.skip = true - yield - ensure - Gem::Deprecate.skip = original - end + def skip_during + original = Gem::Deprecate.skip + Gem::Deprecate.skip = true + yield + ensure + Gem::Deprecate.skip = original + end - def self.next_rubygems_major_version # :nodoc: - Gem::Version.new(Gem.rubygems_version.segments.first).bump - end + def self.next_rubygems_major_version # :nodoc: + Gem::Version.new(Gem.rubygems_version.segments.first).bump + end - ## - # Simple deprecation method that deprecates +name+ by wrapping it up - # in a dummy method. It warns on each call to the dummy method - # telling the user of +repl+ (unless +repl+ is :none) and the - # year/month that it is planned to go away. + ## + # Simple deprecation method that deprecates +name+ by wrapping it up + # in a dummy method. It warns on each call to the dummy method + # telling the user of +repl+ (unless +repl+ is :none) and the + # year/month that it is planned to go away. - def deprecate(name, repl, year, month) - class_eval do - old = "_deprecated_#{name}" - alias_method old, name - define_method name do |*args, &block| - klass = is_a? Module - target = klass ? "#{self}." : "#{self.class}#" - msg = [ - "NOTE: #{target}#{name} is deprecated", - repl == :none ? " with no replacement" : "; use #{repl} instead", - format(". It will be removed on or after %4d-%02d.", year, month), - "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", - ] - warn "#{msg.join}." unless Gem::Deprecate.skip - send old, *args, &block + def deprecate(name, repl, year, month) + class_eval do + old = "_deprecated_#{name}" + alias_method old, name + define_method name do |*args, &block| + klass = is_a? Module + target = klass ? "#{self}." : "#{self.class}#" + msg = [ + "NOTE: #{target}#{name} is deprecated", + repl == :none ? " with no replacement" : "; use #{repl} instead", + format(". It will be removed on or after %4d-%02d.", year, month), + "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", + ] + warn "#{msg.join}." unless Gem::Deprecate.skip + send old, *args, &block + end + ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - end - ## - # Simple deprecation method that deprecates +name+ by wrapping it up - # in a dummy method. It warns on each call to the dummy method - # telling the user of +repl+ (unless +repl+ is :none) and the - # Rubygems version that it is planned to go away. + ## + # Simple deprecation method that deprecates +name+ by wrapping it up + # in a dummy method. It warns on each call to the dummy method + # telling the user of +repl+ (unless +repl+ is :none) and the + # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none) - class_eval do - old = "_deprecated_#{name}" - alias_method old, name - define_method name do |*args, &block| - klass = is_a? Module - target = klass ? "#{self}." : "#{self.class}#" - msg = [ - "NOTE: #{target}#{name} is deprecated", - replacement == :none ? " with no replacement" : "; use #{replacement} instead", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", - "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", - ] - warn "#{msg.join}." unless Gem::Deprecate.skip - send old, *args, &block + def rubygems_deprecate(name, replacement=:none) + class_eval do + old = "_deprecated_#{name}" + alias_method old, name + define_method name do |*args, &block| + klass = is_a? Module + target = klass ? "#{self}." : "#{self.class}#" + msg = [ + "NOTE: #{target}#{name} is deprecated", + replacement == :none ? " with no replacement" : "; use #{replacement} instead", + ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", + "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", + ] + warn "#{msg.join}." unless Gem::Deprecate.skip + send old, *args, &block + end + ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - end - # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) - class_eval do - define_method "deprecated?" do - true - end + # Deprecation method to deprecate Rubygems commands + def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) + class_eval do + define_method "deprecated?" do + true + end - define_method "deprecation_warning" do - msg = [ - "#{command} command is deprecated", - ". It will be removed in Rubygems #{version}.\n", - ] + define_method "deprecation_warning" do + msg = [ + "#{command} command is deprecated", + ". It will be removed in Rubygems #{version}.\n", + ] - alert_warning msg.join.to_s unless Gem::Deprecate.skip + alert_warning msg.join.to_s unless Gem::Deprecate.skip + end end end - end - module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during + module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during + end end diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 86a0e73f28..09ad1407c2 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -184,6 +184,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder end def cargo_dylib_path(dest_path, crate_name) + so_ext = RbConfig::CONFIG["SOEXT"] prefix = so_ext == "dll" ? "" : "lib" path_parts = [dest_path] path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"] @@ -312,22 +313,6 @@ EOF deffile_path end - # We have to basically reimplement <code>RbConfig::CONFIG['SOEXT']</code> here to support - # Ruby < 2.5 - # - # @see https://github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185 - def so_ext - return RbConfig::CONFIG["SOEXT"] if RbConfig::CONFIG.key?("SOEXT") - - if win_target? - "dll" - elsif darwin_target? - "dylib" - else - "so" - end - end - # Corresponds to $(LIBPATH) in mkmf def mkmf_libpath ["-L", "native=#{makefile_config("libdir")}"] diff --git a/lib/rubygems/gemcutter_utilities/webauthn_poller.rb b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb index 0fdd1d5bf4..fe3f163a88 100644 --- a/lib/rubygems/gemcutter_utilities/webauthn_poller.rb +++ b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb @@ -69,8 +69,10 @@ module Gem::GemcutterUtilities rubygems_api_request(:get, "api/v1/webauthn_verification/#{webauthn_token}/status.json") do |request| if credentials.empty? request.add_field "Authorization", api_key + elsif credentials[:identifier] && credentials[:password] + request.basic_auth credentials[:identifier], credentials[:password] else - request.basic_auth credentials[:email], credentials[:password] + raise Gem::WebauthnVerificationError, "Provided missing credentials" end end end diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 1d5d764237..c855423ed7 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -7,7 +7,6 @@ # rubocop:enable Style/AsciiComments -require_relative "../rubygems" require_relative "security" require_relative "user_interaction" @@ -295,7 +294,6 @@ class Gem::Package Gem.load_yaml - @spec.mark_version @spec.validate true, strict_validation unless skip_validation setup_signer( @@ -528,12 +526,13 @@ EOM # Loads a Gem::Specification from the TarEntry +entry+ def load_spec(entry) # :nodoc: + limit = 10 * 1024 * 1024 case entry.full_name when "metadata" then - @spec = Gem::Specification.from_yaml entry.read + @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit) when "metadata.gz" then Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| - @spec = Gem::Specification.from_yaml gzio.read + @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit) end end end @@ -557,7 +556,7 @@ EOM @checksums = gem.seek "checksums.yaml.gz" do |entry| Zlib::GzipReader.wrap entry do |gz_io| - Gem::SafeYAML.safe_load gz_io.read + Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024) end end end @@ -664,7 +663,7 @@ EOM case file_name when /\.sig$/ then - @signatures[$`] = entry.read if @security_policy + @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy return else digest entry @@ -724,6 +723,12 @@ EOM IO.copy_stream(src, dst) end end + + def limit_read(io, name, limit) + bytes = io.read(limit + 1) + raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit + bytes + end end require_relative "package/digest_io" 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 48b7344aee..d54ad12880 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -134,6 +134,7 @@ class Gem::Platform when /netbsdelf/ then ["netbsdelf", nil] when /openbsd(\d+\.\d+)?/ then ["openbsd", $1] when /solaris(\d+\.\d+)?/ then ["solaris", $1] + when /wasi/ then ["wasi", nil] # test when /^(\w+_platform)(\d+)?/ then [$1, $2] else ["unknown", nil] diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 29139cf725..eb84609529 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" @@ -179,22 +180,12 @@ class Gem::Specification < Gem::BasicSpecification @@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=].freeze # :nodoc: + REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc: def removed_method_calls @removed_method_calls ||= [] 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: @@ -788,26 +779,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 +794,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 +813,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 +821,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: @@ -893,23 +850,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 ## @@ -923,27 +871,17 @@ class Gem::Specification < Gem::BasicSpecification 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 +906,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 +916,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 @@ -1033,12 +969,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 +977,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 +1052,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 +1197,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 +1218,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 } @@ -1874,8 +1808,6 @@ class Gem::Specification < Gem::BasicSpecification end def encode_with(coder) # :nodoc: - mark_version - coder.add "name", @name coder.add "version", @version platform = case @original_platform @@ -2171,13 +2103,6 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Sets the rubygems_version to the current RubyGems version. - - def mark_version - @rubygems_version = Gem::VERSION - end - - ## # Track removed method calls to warn about during build time. # Warn about unknown attributes while loading a spec. @@ -2494,7 +2419,6 @@ class Gem::Specification < Gem::BasicSpecification # still have their default values are omitted. def to_ruby - mark_version result = [] result << "# -*- encoding: utf-8 -*-" result << "#{Gem::StubSpecification::PREFIX}#{name} #{version} #{platform} #{raw_require_paths.join("\0")}" diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 516c26f53c..812b0f889e 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -274,7 +274,9 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: return if rubygems_version == Gem::VERSION - error "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}" + warning "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}" + + @specification.rubygems_version = Gem::VERSION end def validate_required_attributes diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb new file mode 100644 index 0000000000..812431fa32 --- /dev/null +++ b/lib/rubygems/specification_record.rb @@ -0,0 +1,181 @@ +# 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) + stubs = stubs.uniq(&:full_name) + 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 + + ## + # 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/uninstaller.rb b/lib/rubygems/uninstaller.rb index c96df2a085..e1f82e6a21 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,8 @@ 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) @force_executables = options[:executables] @force_all = options[:all] @force_ignore = options[:ignore] @@ -70,7 +70,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 = [] @@ -284,17 +284,18 @@ 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) + specification_record = @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record + 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 ## @@ -406,4 +407,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/syntax_suggest/clean_document.rb b/lib/syntax_suggest/clean_document.rb index 0847a62e27..2790ccae86 100644 --- a/lib/syntax_suggest/clean_document.rb +++ b/lib/syntax_suggest/clean_document.rb @@ -267,7 +267,7 @@ module SyntaxSuggest groups.each do |lines| line = lines.first - # Handle the case of multiple groups in a a row + # Handle the case of multiple groups in a row # if one is already replaced, move on next if @document[line.index].empty? diff --git a/lib/time.rb b/lib/time.rb index 970932a200..b565130b50 100644 --- a/lib/time.rb +++ b/lib/time.rb @@ -391,6 +391,8 @@ class Time # heuristic to detect the format of the input string, you provide # a second argument that describes the format of the string. # + # Raises `ArgumentError` if the date or format is invalid. + # # If a block is given, the year described in +date+ is converted by the # block. For example: # |