summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/benchmark.gemspec2
-rw-r--r--lib/bundled_gems.rb120
-rw-r--r--lib/bundler.rb9
-rw-r--r--lib/bundler/cli.rb54
-rw-r--r--lib/bundler/cli/add.rb2
-rw-r--r--lib/bundler/cli/check.rb2
-rw-r--r--lib/bundler/cli/fund.rb2
-rw-r--r--lib/bundler/cli/gem.rb14
-rw-r--r--lib/bundler/cli/install.rb19
-rw-r--r--lib/bundler/cli/lock.rb10
-rw-r--r--lib/bundler/cli/outdated.rb2
-rw-r--r--lib/bundler/compact_index_client.rb131
-rw-r--r--lib/bundler/compact_index_client/cache.rb119
-rw-r--r--lib/bundler/compact_index_client/cache_file.rb5
-rw-r--r--lib/bundler/compact_index_client/parser.rb84
-rw-r--r--lib/bundler/compact_index_client/updater.rb11
-rw-r--r--lib/bundler/constants.rb7
-rw-r--r--lib/bundler/definition.rb152
-rw-r--r--lib/bundler/dsl.rb44
-rw-r--r--lib/bundler/endpoint_specification.rb11
-rw-r--r--lib/bundler/env.rb2
-rw-r--r--lib/bundler/errors.rb12
-rw-r--r--lib/bundler/fetcher.rb4
-rw-r--r--lib/bundler/fetcher/compact_index.rb39
-rw-r--r--lib/bundler/force_platform.rb2
-rw-r--r--lib/bundler/gem_helper.rb2
-rw-r--r--lib/bundler/gem_helpers.rb21
-rw-r--r--lib/bundler/injector.rb5
-rw-r--r--lib/bundler/inline.rb23
-rw-r--r--lib/bundler/installer.rb36
-rw-r--r--lib/bundler/installer/gem_installer.rb7
-rw-r--r--lib/bundler/installer/parallel_installer.rb5
-rw-r--r--lib/bundler/installer/standalone.rb3
-rw-r--r--lib/bundler/lockfile_parser.rb2
-rw-r--r--lib/bundler/man/bundle-add.143
-rw-r--r--lib/bundler/man/bundle-add.1.ronn51
-rw-r--r--lib/bundler/man/bundle-binstubs.12
-rw-r--r--lib/bundler/man/bundle-cache.12
-rw-r--r--lib/bundler/man/bundle-check.12
-rw-r--r--lib/bundler/man/bundle-clean.12
-rw-r--r--lib/bundler/man/bundle-config.14
-rw-r--r--lib/bundler/man/bundle-config.1.ronn2
-rw-r--r--lib/bundler/man/bundle-console.12
-rw-r--r--lib/bundler/man/bundle-doctor.12
-rw-r--r--lib/bundler/man/bundle-exec.12
-rw-r--r--lib/bundler/man/bundle-gem.18
-rw-r--r--lib/bundler/man/bundle-gem.1.ronn11
-rw-r--r--lib/bundler/man/bundle-help.12
-rw-r--r--lib/bundler/man/bundle-info.12
-rw-r--r--lib/bundler/man/bundle-init.12
-rw-r--r--lib/bundler/man/bundle-inject.12
-rw-r--r--lib/bundler/man/bundle-install.12
-rw-r--r--lib/bundler/man/bundle-list.12
-rw-r--r--lib/bundler/man/bundle-lock.12
-rw-r--r--lib/bundler/man/bundle-open.12
-rw-r--r--lib/bundler/man/bundle-outdated.12
-rw-r--r--lib/bundler/man/bundle-platform.12
-rw-r--r--lib/bundler/man/bundle-plugin.12
-rw-r--r--lib/bundler/man/bundle-pristine.12
-rw-r--r--lib/bundler/man/bundle-remove.12
-rw-r--r--lib/bundler/man/bundle-show.12
-rw-r--r--lib/bundler/man/bundle-update.12
-rw-r--r--lib/bundler/man/bundle-version.12
-rw-r--r--lib/bundler/man/bundle-viz.12
-rw-r--r--lib/bundler/man/bundle.12
-rw-r--r--lib/bundler/man/gemfile.54
-rw-r--r--lib/bundler/man/gemfile.5.ronn6
-rw-r--r--lib/bundler/plugin/api/source.rb3
-rw-r--r--lib/bundler/resolver.rb67
-rw-r--r--lib/bundler/resolver/base.rb10
-rw-r--r--lib/bundler/resolver/candidate.rb20
-rw-r--r--lib/bundler/resolver/package.rb15
-rw-r--r--lib/bundler/resolver/spec_group.rb22
-rw-r--r--lib/bundler/retry.rb2
-rw-r--r--lib/bundler/ruby_version.rb8
-rw-r--r--lib/bundler/rubygems_ext.rb129
-rw-r--r--lib/bundler/rubygems_gem_installer.rb40
-rw-r--r--lib/bundler/rubygems_integration.rb33
-rw-r--r--lib/bundler/runtime.rb7
-rw-r--r--lib/bundler/self_manager.rb26
-rw-r--r--lib/bundler/settings.rb20
-rw-r--r--lib/bundler/setup.rb3
-rw-r--r--lib/bundler/shared_helpers.rb4
-rw-r--r--lib/bundler/source/git.rb81
-rw-r--r--lib/bundler/source/git/git_proxy.rb14
-rw-r--r--lib/bundler/source/path.rb15
-rw-r--r--lib/bundler/source/rubygems.rb59
-rw-r--r--lib/bundler/source_list.rb15
-rw-r--r--lib/bundler/spec_set.rb28
-rw-r--r--lib/bundler/stub_specification.rb8
-rw-r--r--lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt106
-rw-r--r--lib/bundler/templates/newgem/README.md.tt8
-rw-r--r--lib/bundler/ui/shell.rb26
-rw-r--r--lib/bundler/ui/silent.rb13
-rw-r--r--lib/bundler/vendor/securerandom/.document1
-rw-r--r--lib/bundler/vendor/securerandom/lib/random/formatter.rb373
-rw-r--r--lib/bundler/vendor/securerandom/lib/securerandom.rb96
-rw-r--r--lib/bundler/vendored_net_http.rb23
-rw-r--r--lib/bundler/vendored_securerandom.rb14
-rw-r--r--lib/bundler/yaml_serializer.rb11
-rw-r--r--lib/error_highlight/base.rb305
-rw-r--r--lib/fileutils.gemspec2
-rw-r--r--lib/find.gemspec2
-rw-r--r--lib/irb.rb81
-rw-r--r--lib/irb/command/base.rb18
-rw-r--r--lib/irb/command/cd.rb51
-rw-r--r--lib/irb/command/debug.rb14
-rw-r--r--lib/irb/command/help.rb2
-rw-r--r--lib/irb/completion.rb38
-rw-r--r--lib/irb/context.rb56
-rw-r--r--lib/irb/debug.rb16
-rw-r--r--lib/irb/default_commands.rb62
-rw-r--r--lib/irb/easter-egg.rb13
-rw-r--r--lib/irb/input-method.rb34
-rw-r--r--lib/irb/inspector.rb61
-rw-r--r--lib/irb/nesting_parser.rb390
-rw-r--r--lib/irb/pager.rb6
-rw-r--r--lib/irb/ruby-lex.rb208
-rw-r--r--lib/irb/ruby_logo.aa112
-rw-r--r--lib/irb/source_finder.rb2
-rw-r--r--lib/irb/statement.rb2
-rw-r--r--lib/irb/version.rb4
-rw-r--r--lib/irb/workspace.rb12
-rw-r--r--lib/logger.rb22
-rw-r--r--lib/logger/log_device.rb9
-rw-r--r--lib/logger/period.rb16
-rw-r--r--lib/logger/version.rb2
-rw-r--r--lib/net/http.rb138
-rw-r--r--lib/net/http/header.rb2
-rw-r--r--lib/net/http/requests.rb5
-rw-r--r--lib/open-uri.rb61
-rw-r--r--lib/optparse.rb16
-rw-r--r--lib/prism.rb2
-rw-r--r--lib/prism/debug.rb249
-rw-r--r--lib/prism/desugar_compiler.rb185
-rw-r--r--lib/prism/ffi.rb54
-rw-r--r--lib/prism/node_ext.rb105
-rw-r--r--lib/prism/parse_result.rb29
-rw-r--r--lib/prism/parse_result/errors.rb65
-rw-r--r--lib/prism/parse_result/newlines.rb57
-rw-r--r--lib/prism/prism.gemspec8
-rw-r--r--lib/prism/translation/parser.rb6
-rw-r--r--lib/prism/translation/parser/compiler.rb284
-rw-r--r--lib/prism/translation/parser/lexer.rb30
-rw-r--r--lib/prism/translation/parser/rubocop.rb73
-rw-r--r--lib/prism/translation/ripper.rb42
-rw-r--r--lib/prism/translation/ruby_parser.rb79
-rw-r--r--lib/random/formatter.rb7
-rw-r--r--lib/rdoc.rb44
-rw-r--r--lib/rdoc/code_object/alias.rb (renamed from lib/rdoc/alias.rb)2
-rw-r--r--lib/rdoc/code_object/anon_class.rb (renamed from lib/rdoc/anon_class.rb)0
-rw-r--r--lib/rdoc/code_object/any_method.rb (renamed from lib/rdoc/any_method.rb)0
-rw-r--r--lib/rdoc/code_object/attr.rb (renamed from lib/rdoc/attr.rb)0
-rw-r--r--lib/rdoc/code_object/class_module.rb (renamed from lib/rdoc/class_module.rb)0
-rw-r--r--lib/rdoc/code_object/constant.rb (renamed from lib/rdoc/constant.rb)0
-rw-r--r--lib/rdoc/code_object/context.rb (renamed from lib/rdoc/context.rb)0
-rw-r--r--lib/rdoc/code_object/context/section.rb (renamed from lib/rdoc/context/section.rb)0
-rw-r--r--lib/rdoc/code_object/extend.rb (renamed from lib/rdoc/extend.rb)0
-rw-r--r--lib/rdoc/code_object/ghost_method.rb (renamed from lib/rdoc/ghost_method.rb)0
-rw-r--r--lib/rdoc/code_object/include.rb (renamed from lib/rdoc/include.rb)0
-rw-r--r--lib/rdoc/code_object/meta_method.rb (renamed from lib/rdoc/meta_method.rb)0
-rw-r--r--lib/rdoc/code_object/method_attr.rb (renamed from lib/rdoc/method_attr.rb)6
-rw-r--r--lib/rdoc/code_object/mixin.rb (renamed from lib/rdoc/mixin.rb)0
-rw-r--r--lib/rdoc/code_object/normal_class.rb (renamed from lib/rdoc/normal_class.rb)0
-rw-r--r--lib/rdoc/code_object/normal_module.rb (renamed from lib/rdoc/normal_module.rb)0
-rw-r--r--lib/rdoc/code_object/require.rb (renamed from lib/rdoc/require.rb)0
-rw-r--r--lib/rdoc/code_object/single_class.rb (renamed from lib/rdoc/single_class.rb)0
-rw-r--r--lib/rdoc/code_object/top_level.rb (renamed from lib/rdoc/top_level.rb)8
-rw-r--r--lib/rdoc/generator/darkfish.rb3
-rw-r--r--lib/rdoc/generator/pot/message_extractor.rb2
-rw-r--r--lib/rdoc/generator/pot/po_entry.rb2
-rw-r--r--lib/rdoc/generator/template/darkfish/_head.rhtml1
-rw-r--r--lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml3
-rw-r--r--lib/rdoc/generator/template/darkfish/class.rhtml17
-rw-r--r--lib/rdoc/generator/template/darkfish/css/rdoc.css534
-rw-r--r--lib/rdoc/generator/template/darkfish/index.rhtml13
-rw-r--r--lib/rdoc/generator/template/darkfish/js/darkfish.js17
-rw-r--r--lib/rdoc/generator/template/darkfish/page.rhtml10
-rw-r--r--lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml18
-rw-r--r--lib/rdoc/generator/template/darkfish/servlet_root.rhtml7
-rw-r--r--lib/rdoc/generator/template/darkfish/table_of_contents.rhtml11
-rw-r--r--lib/rdoc/markdown.rb34
-rw-r--r--lib/rdoc/markup.rb31
-rw-r--r--lib/rdoc/markup/attribute_manager.rb4
-rw-r--r--lib/rdoc/markup/pre_process.rb15
-rw-r--r--lib/rdoc/markup/to_bs.rb2
-rw-r--r--lib/rdoc/options.rb8
-rw-r--r--lib/rdoc/parser.rb3
-rw-r--r--lib/rdoc/parser/c.rb2
-rw-r--r--lib/rdoc/parser/changelog.rb4
-rw-r--r--lib/rdoc/parser/prism_ruby.rb1026
-rw-r--r--lib/rdoc/parser/ripper_state_lex.rb312
-rw-r--r--lib/rdoc/parser/ruby.rb21
-rw-r--r--lib/rdoc/rd/block_parser.rb10
-rw-r--r--lib/rdoc/rd/inline_parser.rb10
-rw-r--r--lib/rdoc/rdoc.gemspec40
-rw-r--r--lib/rdoc/rdoc.rb4
-rw-r--r--lib/rdoc/ri/driver.rb16
-rw-r--r--lib/rdoc/task.rb5
-rw-r--r--lib/rdoc/tom_doc.rb8
-rw-r--r--lib/rdoc/version.rb2
-rw-r--r--lib/reline.rb169
-rw-r--r--lib/reline/config.rb100
-rw-r--r--lib/reline/face.rb2
-rw-r--r--lib/reline/general_io.rb111
-rw-r--r--lib/reline/io.rb41
-rw-r--r--lib/reline/io/ansi.rb (renamed from lib/reline/ansi.rb)182
-rw-r--r--lib/reline/io/dumb.rb106
-rw-r--r--lib/reline/io/windows.rb (renamed from lib/reline/windows.rb)214
-rw-r--r--lib/reline/key_actor.rb1
-rw-r--r--lib/reline/key_actor/base.rb28
-rw-r--r--lib/reline/key_actor/composite.rb17
-rw-r--r--lib/reline/key_actor/emacs.rb8
-rw-r--r--lib/reline/key_actor/vi_command.rb4
-rw-r--r--lib/reline/key_actor/vi_insert.rb4
-rw-r--r--lib/reline/key_stroke.rb169
-rw-r--r--lib/reline/line_editor.rb220
-rw-r--r--lib/reline/terminfo.rb7
-rw-r--r--lib/reline/unicode.rb53
-rw-r--r--lib/reline/unicode/east_asian_width.rb2453
-rw-r--r--lib/reline/version.rb2
-rw-r--r--lib/resolv.gemspec1
-rw-r--r--lib/resolv.rb44
-rw-r--r--lib/ruby_vm/rjit/assembler.rb20
-rw-r--r--lib/ruby_vm/rjit/insn_compiler.rb52
-rw-r--r--lib/rubygems.rb82
-rw-r--r--lib/rubygems/basic_specification.rb39
-rw-r--r--lib/rubygems/bundler_version_finder.rb2
-rw-r--r--lib/rubygems/commands/exec_command.rb7
-rw-r--r--lib/rubygems/commands/fetch_command.rb14
-rw-r--r--lib/rubygems/commands/install_command.rb4
-rw-r--r--lib/rubygems/commands/pristine_command.rb21
-rw-r--r--lib/rubygems/commands/setup_command.rb10
-rw-r--r--lib/rubygems/commands/uninstall_command.rb13
-rw-r--r--lib/rubygems/config_file.rb21
-rw-r--r--lib/rubygems/dependency.rb22
-rw-r--r--lib/rubygems/exceptions.rb5
-rw-r--r--lib/rubygems/ext/builder.rb10
-rw-r--r--lib/rubygems/ext/cargo_builder.rb7
-rw-r--r--lib/rubygems/ext/cmake_builder.rb9
-rw-r--r--lib/rubygems/ext/configure_builder.rb9
-rw-r--r--lib/rubygems/ext/ext_conf_builder.rb13
-rw-r--r--lib/rubygems/ext/rake_builder.rb7
-rw-r--r--lib/rubygems/install_update_options.rb5
-rw-r--r--lib/rubygems/installer.rb21
-rw-r--r--lib/rubygems/platform.rb11
-rw-r--r--lib/rubygems/query_utils.rb2
-rw-r--r--lib/rubygems/remote_fetcher.rb1
-rw-r--r--lib/rubygems/requirement.rb8
-rw-r--r--lib/rubygems/resolver/activation_request.rb2
-rw-r--r--lib/rubygems/resolver/best_set.rb30
-rw-r--r--lib/rubygems/source.rb18
-rw-r--r--lib/rubygems/source/git.rb12
-rw-r--r--lib/rubygems/source/installed.rb4
-rw-r--r--lib/rubygems/source/local.rb12
-rw-r--r--lib/rubygems/source/specific_file.rb8
-rw-r--r--lib/rubygems/specification.rb172
-rw-r--r--lib/rubygems/specification_policy.rb20
-rw-r--r--lib/rubygems/specification_record.rb212
-rw-r--r--lib/rubygems/stub_specification.rb21
-rw-r--r--lib/rubygems/target_rbconfig.rb50
-rw-r--r--lib/rubygems/uninstaller.rb54
-rw-r--r--lib/rubygems/util/licenses.rb25
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/https.rb2
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse.rb2
-rw-r--r--lib/rubygems/vendor/resolv/lib/resolv.rb6
-rw-r--r--lib/rubygems/vendor/securerandom/.document1
-rw-r--r--lib/rubygems/vendor/securerandom/lib/random/formatter.rb373
-rw-r--r--lib/rubygems/vendor/securerandom/lib/securerandom.rb96
-rw-r--r--lib/rubygems/vendored_securerandom.rb4
-rw-r--r--lib/rubygems/version.rb4
-rw-r--r--lib/rubygems/yaml_serializer.rb11
-rw-r--r--lib/set.rb20
-rw-r--r--lib/shellwords.gemspec5
-rw-r--r--lib/shellwords.rb11
-rw-r--r--lib/tempfile.rb272
-rw-r--r--lib/time.rb61
-rw-r--r--lib/timeout.rb6
-rw-r--r--lib/tmpdir.rb6
-rw-r--r--lib/uri/common.rb50
-rw-r--r--lib/uri/file.rb6
-rw-r--r--lib/uri/generic.rb24
-rw-r--r--lib/uri/rfc2396_parser.rb6
-rw-r--r--lib/uri/rfc3986_parser.rb29
-rw-r--r--lib/uri/version.rb2
285 files changed, 8703 insertions, 5019 deletions
diff --git a/lib/benchmark.gemspec b/lib/benchmark.gemspec
index d6e98db805..35deff8d18 100644
--- a/lib/benchmark.gemspec
+++ b/lib/benchmark.gemspec
@@ -16,6 +16,8 @@ Gem::Specification.new do |spec|
spec.homepage = "https://github.com/ruby/benchmark"
spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.required_ruby_version = ">= 2.1.0"
+
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb
index c933ad0471..a0e2c4ef8c 100644
--- a/lib/bundled_gems.rb
+++ b/lib/bundled_gems.rb
@@ -29,24 +29,19 @@ module Gem::BUNDLED_GEMS
"ostruct" => "3.5.0",
"pstore" => "3.5.0",
"rdoc" => "3.5.0",
+ "win32ole" => "3.5.0",
+ "fiddle" => "3.5.0",
+ "logger" => "3.5.0",
+ "benchmark" => "3.5.0",
+ "irb" => "3.5.0",
+ "reline" => "3.5.0",
+ # "readline" => "3.5.0", # This is wrapper for reline. We don't warn for this.
}.freeze
+ SINCE_FAST_PATH = SINCE.transform_keys { |g| g.sub(/\A.*\-/, "") }.freeze
+
EXACT = {
- "abbrev" => true,
- "base64" => true,
- "bigdecimal" => true,
- "csv" => true,
- "drb" => true,
- "getoptlong" => true,
- "mutex_m" => true,
- "nkf" => true, "kconv" => "nkf",
- "observer" => true,
- "resolv-replace" => true,
- "rinda" => true,
- "syslog" => true,
- "ostruct" => true,
- "pstore" => true,
- "rdoc" => true,
+ "kconv" => "nkf",
}.freeze
PREFIXED = {
@@ -74,8 +69,12 @@ module Gem::BUNDLED_GEMS
[::Kernel.singleton_class, ::Kernel].each do |kernel_class|
kernel_class.send(:alias_method, :no_warning_require, :require)
kernel_class.send(:define_method, :require) do |name|
- if message = ::Gem::BUNDLED_GEMS.warning?(name, specs: spec_names) # rubocop:disable Style/HashSyntax
- warn message, :uplevel => 1
+ if message = ::Gem::BUNDLED_GEMS.warning?(name, specs: spec_names)
+ if ::Gem::BUNDLED_GEMS.uplevel > 0
+ Kernel.warn message, uplevel: ::Gem::BUNDLED_GEMS.uplevel
+ else
+ Kernel.warn message
+ end
end
kernel_class.send(:no_warning_require, name)
end
@@ -87,6 +86,36 @@ module Gem::BUNDLED_GEMS
end
end
+ def self.uplevel
+ frame_count = 0
+ frames_to_skip = 3
+ uplevel = 0
+ require_found = false
+ Thread.each_caller_location do |cl|
+ frame_count += 1
+ if frames_to_skip >= 1
+ frames_to_skip -= 1
+ next
+ end
+ uplevel += 1
+ if require_found
+ if cl.base_label != "require"
+ return uplevel
+ end
+ else
+ if cl.base_label == "require"
+ require_found = true
+ end
+ end
+ # Don't show script name when bundle exec and call ruby script directly.
+ if cl.path.end_with?("bundle")
+ frame_count = 0
+ break
+ end
+ end
+ require_found ? 1 : frame_count - 1
+ end
+
def self.find_gem(path)
if !path
return
@@ -97,12 +126,33 @@ module Gem::BUNDLED_GEMS
else
return
end
- EXACT[n] or PREFIXED[n = n[%r[\A[^/]+(?=/)]]] && n
+ (EXACT[n] || !!SINCE[n]) or PREFIXED[n = n[%r[\A[^/]+(?=/)]]] && n
end
def self.warning?(name, specs: nil)
# name can be a feature name or a file path with String or Pathname
feature = File.path(name)
+
+ # irb already has reline as a dependency on gemspec, so we don't want to warn about it.
+ # We should update this with a more general solution when we have another case.
+ # ex: Gem.loaded_specs[called_gem].dependencies.any? {|d| d.name == feature }
+ return false if feature.start_with?("reline") && caller_locations(2, 1)[0].to_s.include?("irb")
+
+ # The actual checks needed to properly identify the gem being required
+ # are costly (see [Bug #20641]), so we first do a much cheaper check
+ # to exclude the vast majority of candidates.
+ if feature.include?("/")
+ # If requiring $LIBDIR/mutex_m.rb, we check SINCE_FAST_PATH["mutex_m"]
+ # We'll fail to warn requires for files that are not the entry point
+ # of the gem, e.g. require "logger/formatter.rb" won't warn.
+ # But that's acceptable because this warning is best effort,
+ # and in the overwhelming majority of cases logger.rb will end
+ # up required.
+ return unless SINCE_FAST_PATH[File.basename(feature, ".*")]
+ else
+ return unless SINCE_FAST_PATH[feature]
+ end
+
# bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`,
# and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`.
name = feature.delete_prefix(ARCHDIR)
@@ -136,20 +186,36 @@ module Gem::BUNDLED_GEMS
end
def self.build_message(gem)
- msg = " #{RUBY_VERSION < SINCE[gem] ? "will no longer be" : "is not"} part of the default gems since Ruby #{SINCE[gem]}."
+ msg = " #{RUBY_VERSION < SINCE[gem] ? "will no longer be" : "is not"} part of the default gems starting from Ruby #{SINCE[gem]}."
if defined?(Bundler)
- msg += " Add #{gem} to your Gemfile or gemspec."
+ msg += "\nYou can add #{gem} to your Gemfile or gemspec to silence this warning."
- # We detect the gem name from caller_locations. We need to skip 2 frames like:
- # lib/ruby/3.3.0+0/bundled_gems.rb:90:in `warning?'",
- # lib/ruby/3.3.0+0/bundler/rubygems_integration.rb:247:in `block (2 levels) in replace_require'",
+ # We detect the gem name from caller_locations. First we walk until we find `require`
+ # then take the first frame that's not from `require`.
#
# Additionally, we need to skip Bootsnap and Zeitwerk if present, these
# gems decorate Kernel#require, so they are not really the ones issuing
# the require call users should be warned about. Those are upwards.
- location = Thread.each_caller_location(2) do |cl|
- break cl.path unless cl.base_label == "require"
+ frames_to_skip = 3
+ location = nil
+ require_found = false
+ Thread.each_caller_location do |cl|
+ if frames_to_skip >= 1
+ frames_to_skip -= 1
+ next
+ end
+
+ if require_found
+ if cl.base_label != "require"
+ location = cl.path
+ break
+ end
+ else
+ if cl.base_label == "require"
+ require_found = true
+ end
+ end
end
if location && File.file?(location) && !location.start_with?(Gem::BUNDLED_GEMS::LIBDIR)
@@ -161,7 +227,7 @@ module Gem::BUNDLED_GEMS
end
end
if caller_gem
- msg += " Also contact author of #{caller_gem} to add #{gem} into its gemspec."
+ msg += "\nAlso please contact the author of #{caller_gem} to request adding #{gem} into its gemspec."
end
end
else
@@ -182,7 +248,7 @@ class LoadError
name = path.tr("/", "-")
if !defined?(Bundler) && Gem::BUNDLED_GEMS::SINCE[name] && !Gem::BUNDLED_GEMS::WARNED[name]
- warn name + Gem::BUNDLED_GEMS.build_message(name)
+ warn name + Gem::BUNDLED_GEMS.build_message(name), uplevel: Gem::BUNDLED_GEMS.uplevel
end
super
end
diff --git a/lib/bundler.rb b/lib/bundler.rb
index 5033109db6..9b00610ff0 100644
--- a/lib/bundler.rb
+++ b/lib/bundler.rb
@@ -42,6 +42,7 @@ module Bundler
autoload :Checksum, File.expand_path("bundler/checksum", __dir__)
autoload :CLI, File.expand_path("bundler/cli", __dir__)
autoload :CIDetector, File.expand_path("bundler/ci_detector", __dir__)
+ autoload :CompactIndexClient, File.expand_path("bundler/compact_index_client", __dir__)
autoload :Definition, File.expand_path("bundler/definition", __dir__)
autoload :Dependency, File.expand_path("bundler/dependency", __dir__)
autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__)
@@ -166,6 +167,10 @@ module Bundler
end
end
+ def auto_switch
+ self_manager.restart_with_locked_bundler_if_needed
+ end
+
# Automatically install dependencies if Bundler.settings[:auto_install] exists.
# This is set through config cmd `bundle config set --global auto_install 1`.
#
@@ -357,7 +362,7 @@ module Bundler
def settings
@settings ||= Settings.new(app_config_path)
rescue GemfileNotFound
- @settings = Settings.new(Pathname.new(".bundle").expand_path)
+ @settings = Settings.new
end
# @return [Hash] Environment present before Bundler was activated
@@ -665,7 +670,7 @@ module Bundler
rescue ScriptError, StandardError => e
msg = "There was an error while loading `#{path.basename}`: #{e.message}"
- raise GemspecError, Dsl::DSLError.new(msg, path, e.backtrace, contents)
+ raise GemspecError, Dsl::DSLError.new(msg, path.to_s, e.backtrace, contents)
end
def configure_gem_path
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb
index 40f19c7fed..013ffcdeed 100644
--- a/lib/bundler/cli.rb
+++ b/lib/bundler/cli.rb
@@ -65,7 +65,7 @@ module Bundler
Bundler.reset_settings_and_root!
end
- Bundler.self_manager.restart_with_locked_bundler_if_needed
+ Bundler.auto_switch
Bundler.settings.set_command_option_if_given :retry, options[:retry]
@@ -110,8 +110,8 @@ module Bundler
default_task(Bundler.feature_flag.default_cli_command)
class_option "no-color", type: :boolean, desc: "Disable colorization in output"
- class_option "retry", type: :numeric, aliases: "-r", banner: "NUM",
- desc: "Specify the number of times you wish to attempt network commands"
+ class_option "retry", type: :numeric, aliases: "-r", banner: "NUM",
+ desc: "Specify the number of times you wish to attempt network commands"
class_option "verbose", type: :boolean, desc: "Enable verbose output mode", aliases: "-V"
def help(cli = nil)
@@ -229,6 +229,8 @@ module Bundler
method_option "system", type: :boolean, banner: "Install to the system location ($BUNDLE_PATH or $GEM_HOME) even if the bundle was previously installed somewhere else for this application"
method_option "trust-policy", alias: "P", type: :string, banner: "Gem trust policy (like gem install -P). Must be one of " +
Bundler.rubygems.security_policy_keys.join("|")
+ method_option "target-rbconfig", type: :string, banner: "rbconfig.rb for the deployment target platform"
+
method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group."
method_option "with", type: :array, banner: "Include gems that are part of the specified named group."
def install
@@ -260,15 +262,15 @@ module Bundler
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
method_option "group", aliases: "-g", type: :array, banner: "Update a specific group"
method_option "jobs", aliases: "-j", type: :numeric, banner: "Specify the number of jobs to run in parallel"
- method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
- method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
+ method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
+ method_option "quiet", type: :boolean, banner: "Only output warnings and errors."
method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)"
method_option "redownload", type: :boolean, aliases: "--force", banner: "Force downloading every gem."
method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock"
method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler"
- method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
- method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
- method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
+ method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version"
+ method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version"
+ method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)"
method_option "pre", type: :boolean, banner: "Always choose the highest allowed version when updating gems, regardless of prerelease status"
method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major"
method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated."
@@ -397,11 +399,11 @@ module Bundler
end
desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache"
- method_option "all", type: :boolean,
- default: Bundler.feature_flag.cache_all?,
- banner: "Include all sources (including path and git)."
+ method_option "all", type: :boolean,
+ default: Bundler.feature_flag.cache_all?,
+ banner: "Include all sources (including path and git)."
method_option "all-platforms", type: :boolean, banner: "Include gems for all platforms present in the lockfile, not only the current one"
- method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)."
+ method_option "cache-path", type: :string, banner: "Specify a different cache path than the default (vendor/cache)."
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
method_option "no-install", type: :boolean, banner: "Don't install the gems, only update the cache."
method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache."
@@ -550,10 +552,13 @@ module Bundler
method_option :rubocop, type: :boolean, desc: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true`."
method_option :changelog, type: :boolean, desc: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`."
method_option :test, type: :string, lazy_default: Bundler.settings["gem.test"] || "", aliases: "-t", banner: "Use the specified test framework for your library",
+ enum: %w[rspec minitest test-unit],
desc: "Generate a test directory for your library, either rspec, minitest or test-unit. Set a default with `bundle config set --global gem.test (rspec|minitest|test-unit)`."
method_option :ci, type: :string, lazy_default: Bundler.settings["gem.ci"] || "",
+ enum: %w[github gitlab circle],
desc: "Generate CI configuration, either GitHub Actions, GitLab CI or CircleCI. Set a default with `bundle config set --global gem.ci (github|gitlab|circle)`"
method_option :linter, type: :string, lazy_default: Bundler.settings["gem.linter"] || "",
+ enum: %w[rubocop standard],
desc: "Add a linter and code formatter, either RuboCop or Standard. Set a default with `bundle config set --global gem.linter (rubocop|standard)`"
method_option :github_username, type: :string, default: Bundler.settings["gem.github_username"], banner: "Set your username on GitHub", desc: "Fill in GitHub username on README so that you don't have to do it manually. Set a default with `bundle config set --global gem.github_username <your_username>`."
@@ -602,7 +607,7 @@ module Bundler
end
desc "inject GEM VERSION", "Add the named gem, with version requirements, to the resolved Gemfile", hide: true
- method_option "source", type: :string, banner: "Install gem from the given source"
+ method_option "source", type: :string, banner: "Install gem from the given source"
method_option "group", type: :string, banner: "Install gem into a bundler group"
def inject(name, version)
SharedHelpers.major_deprecation 2, "The `inject` command has been replaced by the `add` command"
@@ -612,16 +617,16 @@ module Bundler
desc "lock", "Creates a lockfile without installing"
method_option "update", type: :array, lazy_default: true, banner: "ignore the existing lockfile, update all gems by default, or update list of given gems"
- method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only"
- method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system"
+ method_option "local", type: :boolean, default: false, banner: "do not attempt to fetch remote gemspecs and use the local gem cache only"
+ method_option "print", type: :boolean, default: false, banner: "print the lockfile to STDOUT instead of writing to the file system"
method_option "gemfile", type: :string, banner: "Use the specified gemfile instead of Gemfile"
method_option "lockfile", type: :string, default: nil, banner: "the path the lockfile should be written to"
method_option "full-index", type: :boolean, default: false, banner: "Fall back to using the single-file index of all gems"
method_option "add-platform", type: :array, default: [], banner: "Add a new platform to the lockfile"
- method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile"
- method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
- method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
- method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)"
+ method_option "remove-platform", type: :array, default: [], banner: "Remove a platform from the lockfile"
+ method_option "patch", type: :boolean, banner: "If updating, prefer updating only to next patch version"
+ method_option "minor", type: :boolean, banner: "If updating, prefer updating only to next minor version"
+ method_option "major", type: :boolean, banner: "If updating, prefer updating to next major version (default)"
method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status"
method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major"
method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated"
@@ -767,13 +772,10 @@ module Bundler
return unless SharedHelpers.md5_available?
- latest = Fetcher::CompactIndex.
- new(nil, Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org")), nil, nil).
- send(:compact_index_client).
- instance_variable_get(:@cache).
- dependencies("bundler").
- map {|d| Gem::Version.new(d.first) }.
- max
+ require_relative "vendored_uri"
+ remote = Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org"))
+ cache_path = Bundler.user_cache.join("compact_index", remote.cache_slug)
+ latest = Bundler::CompactIndexClient.new(cache_path).latest_version("bundler")
return unless latest
current = Gem::Version.new(VERSION)
diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb
index 002d9e1d33..2b300e1783 100644
--- a/lib/bundler/cli/add.rb
+++ b/lib/bundler/cli/add.rb
@@ -34,7 +34,7 @@ module Bundler
end
def validate_options!
- raise InvalidOption, "You can not specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic]
+ raise InvalidOption, "You cannot specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic]
# raise error when no gems are specified
raise InvalidOption, "Please specify gems to add." if gems.empty?
diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb
index 33d31cdd27..2adf59d5d5 100644
--- a/lib/bundler/cli/check.rb
+++ b/lib/bundler/cli/check.rb
@@ -17,7 +17,7 @@ module Bundler
begin
definition.resolve_only_locally!
not_installed = definition.missing_specs
- rescue GemNotFound, SolveFailure
+ rescue GemNotFound, GitError, SolveFailure
Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
Bundler.ui.warn "Install missing gems with `bundle install`."
exit 1
diff --git a/lib/bundler/cli/fund.rb b/lib/bundler/cli/fund.rb
index 52db5aef68..ad7f31f3d6 100644
--- a/lib/bundler/cli/fund.rb
+++ b/lib/bundler/cli/fund.rb
@@ -16,7 +16,7 @@ module Bundler
deps = if groups.any?
Bundler.definition.dependencies_for(groups)
else
- Bundler.definition.current_dependencies
+ Bundler.definition.requested_dependencies
end
fund_info = deps.each_with_object([]) do |dep, arr|
diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb
index b6571d0e86..fb0a184e5d 100644
--- a/lib/bundler/cli/gem.rb
+++ b/lib/bundler/cli/gem.rb
@@ -79,7 +79,7 @@ module Bundler
ensure_safe_gem_name(name, constant_array)
templates = {
- "#{Bundler.preferred_gemfile_name}.tt" => Bundler.preferred_gemfile_name,
+ "Gemfile.tt" => Bundler.preferred_gemfile_name,
"lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb",
"lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb",
"sig/newgem.rbs.tt" => "sig/#{namespaced_path}.rbs",
@@ -191,7 +191,10 @@ module Bundler
templates.merge!("standard.yml.tt" => ".standard.yml")
end
- templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe]
+ if config[:exe]
+ templates.merge!("exe/newgem.tt" => "exe/#{name}")
+ executables.push("exe/#{name}")
+ end
if extension == "c"
templates.merge!(
@@ -275,6 +278,7 @@ module Bundler
end
def ask_and_set_test_framework
+ return if skip?(:test)
test_framework = options[:test] || Bundler.settings["gem.test"]
if test_framework.to_s.empty?
@@ -300,6 +304,10 @@ module Bundler
test_framework
end
+ def skip?(option)
+ options.key?(option) && options[option].nil?
+ end
+
def hint_text(setting)
if Bundler.settings["gem.#{setting}"] == false
"Your choice will only be applied to this gem."
@@ -310,6 +318,7 @@ module Bundler
end
def ask_and_set_ci
+ return if skip?(:ci)
ci_template = options[:ci] || Bundler.settings["gem.ci"]
if ci_template.to_s.empty?
@@ -341,6 +350,7 @@ module Bundler
end
def ask_and_set_linter
+ return if skip?(:linter)
linter_template = options[:linter] || Bundler.settings["gem.linter"]
linter_template = deprecated_rubocop_option if linter_template.nil?
diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb
index 6c102d537d..b0b354cf10 100644
--- a/lib/bundler/cli/install.rb
+++ b/lib/bundler/cli/install.rb
@@ -12,22 +12,31 @@ module Bundler
warn_if_root
- Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed
+ if options[:local]
+ Bundler.self_manager.restart_with_locked_bundler_if_needed
+ else
+ Bundler.self_manager.install_locked_bundler_and_restart_with_it_if_needed
+ end
- Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Bundler::FREEBSD
+ Bundler::SharedHelpers.set_env "RB_USER_INSTALL", "1" if Gem.freebsd_platform?
# Disable color in deployment mode
Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment]
+ if target_rbconfig_path = options[:"target-rbconfig"]
+ Bundler.rubygems.set_target_rbconfig(target_rbconfig_path)
+ end
+
check_for_options_conflicts
check_trust_policy
if options[:deployment] || options[:frozen] || Bundler.frozen_bundle?
unless Bundler.default_lockfile.exist?
- flag = "--deployment flag" if options[:deployment]
- flag ||= "--frozen flag" if options[:frozen]
- flag ||= "deployment setting"
+ flag = "--deployment flag" if options[:deployment]
+ flag ||= "--frozen flag" if options[:frozen]
+ flag ||= "deployment setting" if Bundler.settings[:deployment]
+ flag ||= "frozen setting" if Bundler.settings[:frozen]
raise ProductionError, "The #{flag} requires a lockfile. Please make " \
"sure you have checked your #{SharedHelpers.relative_lockfile_path} into version control " \
"before deploying."
diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb
index dac3d2a09a..3f204bdc45 100644
--- a/lib/bundler/cli/lock.rb
+++ b/lib/bundler/cli/lock.rb
@@ -15,8 +15,8 @@ module Bundler
end
print = options[:print]
- previous_ui_level = Bundler.ui.level
- Bundler.ui.level = "silent" if print
+ previous_output_stream = Bundler.ui.output_stream
+ Bundler.ui.output_stream = :stderr if print
Bundler::Fetcher.disable_endpoint = options["full-index"]
@@ -48,8 +48,8 @@ module Bundler
options["add-platform"].each do |platform_string|
platform = Gem::Platform.new(platform_string)
if platform.to_s == "unknown"
- Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \
- "and adding it will likely lead to resolution errors"
+ Bundler.ui.error "The platform `#{platform_string}` is unknown to RubyGems and can't be added to the lockfile."
+ exit 1
end
definition.add_platform(platform)
end
@@ -68,7 +68,7 @@ module Bundler
end
end
- Bundler.ui.level = previous_ui_level
+ Bundler.ui.output_stream = previous_output_stream
end
end
end
diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb
index ec42e631bb..64a83fd57e 100644
--- a/lib/bundler/cli/outdated.rb
+++ b/lib/bundler/cli/outdated.rb
@@ -54,7 +54,7 @@ module Bundler
end
if options[:parseable]
- Bundler.ui.silence(&definition_resolution)
+ Bundler.ui.progress(&definition_resolution)
else
definition_resolution.call
end
diff --git a/lib/bundler/compact_index_client.rb b/lib/bundler/compact_index_client.rb
index 68e0d7e0d5..a4f5bb1638 100644
--- a/lib/bundler/compact_index_client.rb
+++ b/lib/bundler/compact_index_client.rb
@@ -4,6 +4,29 @@ require "pathname"
require "set"
module Bundler
+ # The CompactIndexClient is responsible for fetching and parsing the compact index.
+ #
+ # The compact index is a set of caching optimized files that are used to fetch gem information.
+ # The files are:
+ # - names: a list of all gem names
+ # - versions: a list of all gem versions
+ # - info/[gem]: a list of all versions of a gem
+ #
+ # The client is instantiated with:
+ # - `directory`: the root directory where the cache files are stored.
+ # - `fetcher`: (optional) an object that responds to #call(uri_path, headers) and returns an http response.
+ # If the `fetcher` is not provided, the client will only read cached files from disk.
+ #
+ # The client is organized into:
+ # - `Updater`: updates the cached files on disk using the fetcher.
+ # - `Cache`: calls the updater, caches files, read and return them from disk
+ # - `Parser`: parses the compact index file data
+ # - `CacheFile`: a concurrency safe file reader/writer that verifies checksums
+ #
+ # The client is intended to optimize memory usage and performance.
+ # It is called 100s or 1000s of times, parsing files with hundreds of thousands of lines.
+ # It may be called concurrently without global interpreter lock in some Rubies.
+ # As a result, some methods may look more complex than necessary to save memory or time.
class CompactIndexClient
# NOTE: MD5 is here not because we expect a server to respond with it, but
# because we use it to generate the etag on first request during the upgrade
@@ -12,6 +35,13 @@ module Bundler
SUPPORTED_DIGESTS = { "sha-256" => :SHA256, "md5" => :MD5 }.freeze
DEBUG_MUTEX = Thread::Mutex.new
+ # info returns an Array of INFO Arrays. Each INFO Array has the following indices:
+ INFO_NAME = 0
+ INFO_VERSION = 1
+ INFO_PLATFORM = 2
+ INFO_DEPS = 3
+ INFO_REQS = 4
+
def self.debug
return unless ENV["DEBUG_COMPACT_INDEX"]
DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
@@ -21,106 +51,47 @@ module Bundler
require_relative "compact_index_client/cache"
require_relative "compact_index_client/cache_file"
+ require_relative "compact_index_client/parser"
require_relative "compact_index_client/updater"
- attr_reader :directory
-
- def initialize(directory, fetcher)
- @directory = Pathname.new(directory)
- @updater = Updater.new(fetcher)
- @cache = Cache.new(@directory)
- @endpoints = Set.new
- @info_checksums_by_name = {}
- @parsed_checksums = false
- @mutex = Thread::Mutex.new
- end
-
- def execution_mode=(block)
- Bundler::CompactIndexClient.debug { "execution_mode=" }
- @endpoints = Set.new
-
- @execution_mode = block
- end
-
- # @return [Lambda] A lambda that takes an array of inputs and a block, and
- # maps the inputs with the block in parallel.
- #
- def execution_mode
- @execution_mode || sequentially
- end
-
- def sequential_execution_mode!
- self.execution_mode = sequentially
- end
-
- def sequentially
- @sequentially ||= lambda do |inputs, &blk|
- inputs.map(&blk)
- end
+ def initialize(directory, fetcher = nil)
+ @cache = Cache.new(directory, fetcher)
+ @parser = Parser.new(@cache)
end
def names
- Bundler::CompactIndexClient.debug { "/names" }
- update("names", @cache.names_path, @cache.names_etag_path)
- @cache.names
+ Bundler::CompactIndexClient.debug { "names" }
+ @parser.names
end
def versions
- Bundler::CompactIndexClient.debug { "/versions" }
- update("versions", @cache.versions_path, @cache.versions_etag_path)
- versions, @info_checksums_by_name = @cache.versions
- versions
+ Bundler::CompactIndexClient.debug { "versions" }
+ @parser.versions
end
def dependencies(names)
Bundler::CompactIndexClient.debug { "dependencies(#{names})" }
- execution_mode.call(names) do |name|
- update_info(name)
- @cache.dependencies(name).map {|d| d.unshift(name) }
- end.flatten(1)
+ names.map {|name| info(name) }
end
- def update_and_parse_checksums!
- Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" }
- return @info_checksums_by_name if @parsed_checksums
- update("versions", @cache.versions_path, @cache.versions_etag_path)
- @info_checksums_by_name = @cache.checksums
- @parsed_checksums = true
- end
-
- private
-
- def update(remote_path, local_path, local_etag_path)
- Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" }
- unless synchronize { @endpoints.add?(remote_path) }
- Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
- return
- end
- @updater.update(url(remote_path), local_path, local_etag_path)
+ def info(name)
+ Bundler::CompactIndexClient.debug { "info(#{name})" }
+ @parser.info(name)
end
- def update_info(name)
- Bundler::CompactIndexClient.debug { "update_info(#{name})" }
- path = @cache.info_path(name)
- unless existing = @info_checksums_by_name[name]
- Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" }
- return
- end
- checksum = SharedHelpers.checksum_for_file(path, :MD5)
- if checksum == existing
- Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" }
- return
- end
- Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" }
- update("info/#{name}", path, @cache.info_etag_path(name))
+ def latest_version(name)
+ Bundler::CompactIndexClient.debug { "latest_version(#{name})" }
+ @parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max
end
- def url(path)
- path
+ def available?
+ Bundler::CompactIndexClient.debug { "available?" }
+ @parser.available?
end
- def synchronize
- @mutex.synchronize { yield }
+ def reset!
+ Bundler::CompactIndexClient.debug { "reset!" }
+ @cache.reset!
end
end
end
diff --git a/lib/bundler/compact_index_client/cache.rb b/lib/bundler/compact_index_client/cache.rb
index 55911fdecf..bedd7f8028 100644
--- a/lib/bundler/compact_index_client/cache.rb
+++ b/lib/bundler/compact_index_client/cache.rb
@@ -7,114 +7,89 @@ module Bundler
class Cache
attr_reader :directory
- def initialize(directory)
+ def initialize(directory, fetcher = nil)
@directory = Pathname.new(directory).expand_path
- info_roots.each {|dir| mkdir(dir) }
- mkdir(info_etag_root)
+ @updater = Updater.new(fetcher) if fetcher
+ @mutex = Thread::Mutex.new
+ @endpoints = Set.new
+
+ @info_root = mkdir("info")
+ @special_characters_info_root = mkdir("info-special-characters")
+ @info_etag_root = mkdir("info-etags")
end
def names
- lines(names_path)
+ fetch("names", names_path, names_etag_path)
end
- def names_path
- directory.join("names")
+ def versions
+ fetch("versions", versions_path, versions_etag_path)
end
- def names_etag_path
- directory.join("names.etag")
- end
+ def info(name, remote_checksum = nil)
+ path = info_path(name)
- def versions
- versions_by_name = Hash.new {|hash, key| hash[key] = [] }
- info_checksums_by_name = {}
-
- lines(versions_path).each do |line|
- name, versions_string, info_checksum = line.split(" ", 3)
- info_checksums_by_name[name] = info_checksum || ""
- versions_string.split(",") do |version|
- delete = version.delete_prefix!("-")
- version = version.split("-", 2).unshift(name)
- if delete
- versions_by_name[name].delete(version)
- else
- versions_by_name[name] << version
- end
- end
+ if remote_checksum && remote_checksum != SharedHelpers.checksum_for_file(path, :MD5)
+ fetch("info/#{name}", path, info_etag_path(name))
+ else
+ Bundler::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum is nil" : "versions index checksum matches local"})" }
+ read(path)
end
-
- [versions_by_name, info_checksums_by_name]
- end
-
- def versions_path
- directory.join("versions")
end
- def versions_etag_path
- directory.join("versions.etag")
+ def reset!
+ @mutex.synchronize { @endpoints.clear }
end
- def checksums
- checksums = {}
-
- lines(versions_path).each do |line|
- name, _, checksum = line.split(" ", 3)
- checksums[name] = checksum
- end
-
- checksums
- end
+ private
- def dependencies(name)
- lines(info_path(name)).map do |line|
- parse_gem(line)
- end
- end
+ def names_path = directory.join("names")
+ def names_etag_path = directory.join("names.etag")
+ def versions_path = directory.join("versions")
+ def versions_etag_path = directory.join("versions.etag")
def info_path(name)
name = name.to_s
+ # TODO: converge this into the info_root by hashing all filenames like info_etag_path
if /[^a-z0-9_-]/.match?(name)
name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}"
- info_roots.last.join(name)
+ @special_characters_info_root.join(name)
else
- info_roots.first.join(name)
+ @info_root.join(name)
end
end
def info_etag_path(name)
name = name.to_s
- info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
+ @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
end
- private
-
- def mkdir(dir)
- SharedHelpers.filesystem_access(dir) do
- FileUtils.mkdir_p(dir)
+ def mkdir(name)
+ directory.join(name).tap do |dir|
+ SharedHelpers.filesystem_access(dir) do
+ FileUtils.mkdir_p(dir)
+ end
end
end
- def lines(path)
- return [] unless path.file?
- lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n")
- header = lines.index("---")
- header ? lines[header + 1..-1] : lines
- end
+ def fetch(remote_path, path, etag_path)
+ if already_fetched?(remote_path)
+ Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
+ else
+ Bundler::CompactIndexClient.debug { "fetching #{remote_path}" }
+ @updater&.update(remote_path, path, etag_path)
+ end
- def parse_gem(line)
- @dependency_parser ||= GemParser.new
- @dependency_parser.parse(line)
+ read(path)
end
- def info_roots
- [
- directory.join("info"),
- directory.join("info-special-characters"),
- ]
+ def already_fetched?(remote_path)
+ @mutex.synchronize { !@endpoints.add?(remote_path) }
end
- def info_etag_root
- directory.join("info-etags")
+ def read(path)
+ return unless path.file?
+ SharedHelpers.filesystem_access(path, :read, &:read)
end
end
end
diff --git a/lib/bundler/compact_index_client/cache_file.rb b/lib/bundler/compact_index_client/cache_file.rb
index 5988bc91b3..299d683438 100644
--- a/lib/bundler/compact_index_client/cache_file.rb
+++ b/lib/bundler/compact_index_client/cache_file.rb
@@ -86,11 +86,6 @@ module Bundler
end
end
- # remove this method when we stop generating md5 digests for legacy etags
- def md5
- @digests && @digests["md5"]
- end
-
def digests?
@digests&.any?
end
diff --git a/lib/bundler/compact_index_client/parser.rb b/lib/bundler/compact_index_client/parser.rb
new file mode 100644
index 0000000000..3276abdd68
--- /dev/null
+++ b/lib/bundler/compact_index_client/parser.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CompactIndexClient
+ class Parser
+ # `compact_index` - an object responding to #names, #versions, #info(name, checksum),
+ # returning the file contents as a string
+ def initialize(compact_index)
+ @compact_index = compact_index
+ @info_checksums = nil
+ @versions_by_name = nil
+ @available = nil
+ @gem_parser = nil
+ end
+
+ def names
+ lines(@compact_index.names)
+ end
+
+ def versions
+ @versions_by_name ||= Hash.new {|hash, key| hash[key] = [] }
+ @info_checksums = {}
+
+ lines(@compact_index.versions).each do |line|
+ name, versions_string, checksum = line.split(" ", 3)
+ @info_checksums[name] = checksum || ""
+ versions_string.split(",") do |version|
+ delete = version.delete_prefix!("-")
+ version = version.split("-", 2).unshift(name)
+ if delete
+ @versions_by_name[name].delete(version)
+ else
+ @versions_by_name[name] << version
+ end
+ end
+ end
+
+ @versions_by_name
+ end
+
+ def info(name)
+ data = @compact_index.info(name, info_checksums[name])
+ lines(data).map {|line| gem_parser.parse(line).unshift(name) }
+ end
+
+ def available?
+ return @available unless @available.nil?
+ @available = !info_checksums.empty?
+ end
+
+ private
+
+ def info_checksums
+ @info_checksums ||= lines(@compact_index.versions).each_with_object({}) do |line, checksums|
+ parse_version_checksum(line, checksums)
+ end
+ end
+
+ def lines(data)
+ return [] if data.nil? || data.empty?
+ lines = data.split("\n")
+ header = lines.index("---")
+ header ? lines[header + 1..-1] : lines
+ end
+
+ def gem_parser
+ @gem_parser ||= GemParser.new
+ end
+
+ # This is mostly the same as `split(" ", 3)` but it avoids allocating extra objects.
+ # This method gets called at least once for every gem when parsing versions.
+ def parse_version_checksum(line, checksums)
+ return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactor index files
+ return unless (checksum_start = line.index(" ", name_end + 1) + 1)
+ checksum_end = line.size - checksum_start
+
+ line.freeze # allows slicing into the string to not allocate a copy of the line
+ name = line[0, name_end]
+ checksum = line[checksum_start, checksum_end]
+ checksums[name.freeze] = checksum # freeze name since it is used as a hash key
+ end
+ end
+ end
+end
diff --git a/lib/bundler/compact_index_client/updater.rb b/lib/bundler/compact_index_client/updater.rb
index 36f6b81db8..88c7146900 100644
--- a/lib/bundler/compact_index_client/updater.rb
+++ b/lib/bundler/compact_index_client/updater.rb
@@ -28,7 +28,6 @@ module Bundler
CacheFile.copy(local_path) do |file|
etag = etag_path.read.tap(&:chomp!) if etag_path.file?
- etag ||= generate_etag(etag_path, file) # Remove this after 2.5.0 has been out for a while.
# Subtract a byte to ensure the range won't be empty.
# Avoids 416 (Range Not Satisfiable) responses.
@@ -67,16 +66,6 @@ module Bundler
etag_path.read.tap(&:chomp!) if etag_path.file?
end
- # When first releasing this opaque etag feature, we want to generate the old MD5 etag
- # based on the content of the file. After that it will always use the saved opaque etag.
- # This transparently saves existing users with good caches from updating a bunch of files.
- # Remove this behavior after 2.5.0 has been out for a while.
- def generate_etag(etag_path, file)
- etag = file.md5.hexdigest
- CacheFile.write(etag_path, etag)
- etag
- end
-
def etag_from_response(response)
return unless response["ETag"]
etag = response["ETag"].delete_prefix("W/")
diff --git a/lib/bundler/constants.rb b/lib/bundler/constants.rb
index bcbd228b18..9564771e78 100644
--- a/lib/bundler/constants.rb
+++ b/lib/bundler/constants.rb
@@ -4,6 +4,11 @@ require "rbconfig"
module Bundler
WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
+ deprecate_constant :WINDOWS
+
FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd")
- NULL = File::NULL
+ deprecate_constant :FREEBSD
+
+ NULL = File::NULL
+ deprecate_constant :NULL
end
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb
index 22070b6b17..75eb5ffa1b 100644
--- a/lib/bundler/definition.rb
+++ b/lib/bundler/definition.rb
@@ -69,7 +69,6 @@ module Bundler
@sources = sources
@unlock = unlock
@optional_groups = optional_groups
- @remote = false
@prefer_local = false
@specs = nil
@ruby_version = ruby_version
@@ -82,7 +81,7 @@ module Bundler
@resolved_bundler_version = nil
@locked_ruby_version = nil
- @new_platform = nil
+ @new_platforms = []
@removed_platform = nil
if lockfile_exists?
@@ -116,7 +115,7 @@ module Bundler
@originally_locked_specs = @locked_specs
@locked_sources = []
@locked_platforms = []
- @locked_checksums = nil
+ @locked_checksums = Bundler.feature_flag.bundler_3_mode?
end
locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) }
@@ -138,7 +137,7 @@ module Bundler
end
@unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version)
- add_current_platform unless Bundler.frozen_bundle?
+ @current_platform_missing = add_current_platform unless Bundler.frozen_bundle?
converge_path_sources_to_gemspec_sources
@path_changes = converge_paths
@@ -164,37 +163,24 @@ module Bundler
end
def resolve_only_locally!
- @remote = false
sources.local_only!
resolve
end
def resolve_with_cache!
+ sources.local!
sources.cached!
resolve
end
def resolve_remotely!
- @remote = true
+ sources.cached!
sources.remote!
resolve
end
- def resolution_mode=(options)
- if options["local"]
- @remote = false
- else
- @remote = true
- @prefer_local = options["prefer-local"]
- end
- end
-
- def setup_sources_for_resolve
- if @remote == false
- sources.cached!
- else
- sources.remote!
- end
+ def prefer_local!
+ @prefer_local = true
end
# For given dependency list returns a SpecSet with Gemspec of all the required
@@ -228,6 +214,7 @@ module Bundler
@resolve = nil
@resolver = nil
@resolution_packages = nil
+ @source_requirements = nil
@specs = nil
Bundler.ui.debug "The definition is missing dependencies, failed to resolve & materialize locally (#{e})"
@@ -310,7 +297,12 @@ module Bundler
end
end
else
- Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}"
+ if lockfile_exists?
+ Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}"
+ else
+ Bundler.ui.debug "Resolving dependencies because there's no lockfile"
+ end
+
start_resolution
end
end
@@ -376,6 +368,10 @@ module Bundler
end
def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
+ return unless Bundler.frozen_bundle?
+
+ raise ProductionError, "Frozen mode is set, but there's no lockfile" unless lockfile_exists?
+
added = []
deleted = []
changed = []
@@ -404,7 +400,7 @@ module Bundler
changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`"
end
- reason = change_reason
+ reason = nothing_changed? ? "some dependencies were deleted from your gemfile" : change_reason
msg = String.new
msg << "#{reason.capitalize.strip}, but the lockfile can't be updated because frozen mode is set"
msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any?
@@ -462,8 +458,10 @@ module Bundler
end
def add_platform(platform)
- @new_platform ||= !@platforms.include?(platform)
- @platforms |= [platform]
+ return if @platforms.include?(platform)
+
+ @new_platforms << platform
+ @platforms << platform
end
def remove_platform(platform)
@@ -479,13 +477,13 @@ module Bundler
end
end
- attr_reader :sources
- private :sources
-
def nothing_changed?
+ return false unless lockfile_exists?
+
!@source_changes &&
!@dependency_changes &&
- !@new_platform &&
+ !@current_platform_missing &&
+ @new_platforms.empty? &&
!@path_changes &&
!@local_changes &&
!@missing_lockfile_dep &&
@@ -502,22 +500,22 @@ module Bundler
@unlocking
end
+ attr_writer :source_requirements
+
private
+ attr_reader :sources
+
def should_add_extra_platforms?
!lockfile_exists? && generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
end
def lockfile_exists?
- file_exists?(lockfile)
- end
-
- def file_exists?(file)
- file && File.exist?(file)
+ lockfile && File.exist?(lockfile)
end
def write_lock(file, preserve_unknown_sections)
- return if Definition.no_lock
+ return if Definition.no_lock || file.nil?
contents = to_lock
@@ -534,7 +532,7 @@ module Bundler
preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler))
- if file_exists?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections)
+ if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections)
return if Bundler.frozen_bundle?
SharedHelpers.filesystem_access(file) { FileUtils.touch(file) }
return
@@ -568,8 +566,8 @@ module Bundler
def resolution_packages
@resolution_packages ||= begin
last_resolve = converge_locked_specs
- remove_invalid_platforms!(current_dependencies)
- packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?)
+ remove_invalid_platforms!
+ packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local)
packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve)
packages = additional_base_requirements_to_force_updates(packages)
packages
@@ -587,7 +585,7 @@ module Bundler
if missing_specs.any?
missing_specs.each do |s|
locked_gem = @locked_specs[s.name].last
- next if locked_gem.nil? || locked_gem.version != s.version || !@remote
+ next if locked_gem.nil? || locked_gem.version != s.version || sources.local_mode?
raise GemNotFound, "Your bundle is locked to #{locked_gem} from #{locked_gem.source}, but that version can " \
"no longer be found in that source. That means the author of #{locked_gem} has removed it. " \
"You'll need to update your bundle to a version other than #{locked_gem} that hasn't been " \
@@ -606,7 +604,7 @@ module Bundler
break if incomplete_specs.empty?
Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies")
- setup_sources_for_resolve
+ sources.remote!
resolution_packages.delete(incomplete_specs)
@resolve = start_resolution
specs = resolve.materialize(dependencies)
@@ -628,12 +626,23 @@ module Bundler
end
def start_resolution
+ local_platform_needed_for_resolvability = @most_specific_non_local_locked_ruby_platform && !@platforms.include?(local_platform)
+ @platforms << local_platform if local_platform_needed_for_resolvability
+ add_platform(Gem::Platform::RUBY) if RUBY_ENGINE == "truffleruby"
+
result = SpecSet.new(resolver.start)
@resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version
- @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms?
- result.complete_platforms!(platforms)
+ if @most_specific_non_local_locked_ruby_platform
+ if spec_set_incomplete_for_platform?(result, @most_specific_non_local_locked_ruby_platform)
+ @platforms.delete(@most_specific_non_local_locked_ruby_platform)
+ elsif local_platform_needed_for_resolvability
+ @platforms.delete(local_platform)
+ end
+ end
+
+ @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms?
SpecSet.new(result.for(dependencies, false, @platforms))
end
@@ -642,26 +651,6 @@ module Bundler
sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source?
end
- def pin_locally_available_names(source_requirements)
- source_requirements.each_with_object({}) do |(name, original_source), new_source_requirements|
- local_source = original_source.dup
- local_source.local_only!
-
- new_source_requirements[name] = if local_source.specs.search(name).any?
- local_source
- else
- original_source
- end
- end
- end
-
- def current_ruby_platform_locked?
- return false unless generic_local_platform_is_ruby?
- return false if Bundler.settings[:force_ruby_platform] && !@platforms.include?(Gem::Platform::RUBY)
-
- current_platform_locked?
- end
-
def current_platform_locked?
@platforms.any? do |bundle_platform|
MatchPlatform.platforms_match?(bundle_platform, local_platform)
@@ -669,9 +658,19 @@ module Bundler
end
def add_current_platform
- return if current_ruby_platform_locked?
+ return if @platforms.include?(local_platform)
+
+ @most_specific_non_local_locked_ruby_platform = find_most_specific_locked_ruby_platform
+ return if @most_specific_non_local_locked_ruby_platform
- add_platform(local_platform)
+ @platforms << local_platform
+ true
+ end
+
+ def find_most_specific_locked_ruby_platform
+ return unless generic_local_platform_is_ruby? && current_platform_locked?
+
+ most_specific_locked_platform
end
def change_reason
@@ -693,7 +692,8 @@ module Bundler
[
[@source_changes, "the list of sources changed"],
[@dependency_changes, "the dependencies in your gemfile changed"],
- [@new_platform, "you added a new platform to your gemfile"],
+ [@current_platform_missing, "your lockfile does not include the current platform"],
+ [@new_platforms.any?, "you added a new platform to your gemfile"],
[@path_changes, "the gemspecs for path gems changed"],
[@local_changes, "the gemspecs for git local gems changed"],
[@missing_lockfile_dep, "your lock file is missing \"#{@missing_lockfile_dep}\""],
@@ -957,17 +957,20 @@ module Bundler
end
def source_requirements
+ @source_requirements ||= find_source_requirements
+ end
+
+ def find_source_requirements
# Record the specs available in each gem's source, so that those
# specs will be available later when the resolver knows where to
# look for that gemspec (or its dependencies)
source_requirements = if precompute_source_requirements_for_indirect_dependencies?
all_requirements = source_map.all_requirements
- all_requirements = pin_locally_available_names(all_requirements) if @prefer_local
{ default: default_source }.merge(all_requirements)
else
{ default: Source::RubygemsAggregate.new(sources, source_map) }.merge(source_map.direct_requirements)
end
- source_requirements.merge!(source_map.locked_requirements) unless @remote
+ source_requirements.merge!(source_map.locked_requirements) if nothing_changed?
metadata_dependencies.each do |dep|
source_requirements[dep.name] = sources.metadata_source
end
@@ -1038,8 +1041,7 @@ module Bundler
def dup_for_full_unlock
unlocked_definition = self.class.new(@lockfile, @dependencies, @sources, true, @ruby_version, @optional_groups, @gemfiles)
- unlocked_definition.resolution_mode = { "local" => !@remote }
- unlocked_definition.setup_sources_for_resolve
+ unlocked_definition.source_requirements = source_requirements
unlocked_definition.gem_version_promoter.tap do |gvp|
gvp.level = gem_version_promoter.level
gvp.strict = gem_version_promoter.strict
@@ -1048,21 +1050,25 @@ module Bundler
unlocked_definition
end
- def remove_invalid_platforms!(dependencies)
+ def remove_invalid_platforms!
return if Bundler.frozen_bundle?
platforms.reverse_each do |platform|
next if local_platform == platform ||
- (@new_platform && platforms.last == platform) ||
+ @new_platforms.include?(platform) ||
@path_changes ||
@dependency_changes ||
- !@originally_locked_specs.incomplete_for_platform?(dependencies, platform)
+ @locked_spec_with_invalid_deps ||
+ !spec_set_incomplete_for_platform?(@originally_locked_specs, platform)
remove_platform(platform)
- add_current_platform if platform == Gem::Platform::RUBY
end
end
+ def spec_set_incomplete_for_platform?(spec_set, platform)
+ spec_set.incomplete_for_platform?(current_dependencies, platform)
+ end
+
def source_map
@source_map ||= SourceMap.new(sources, dependencies, @locked_specs)
end
diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb
index 6af80fb31f..aad8759652 100644
--- a/lib/bundler/dsl.rb
+++ b/lib/bundler/dsl.rb
@@ -42,20 +42,20 @@ module Bundler
end
def eval_gemfile(gemfile, contents = nil)
- expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
- original_gemfile = @gemfile
- @gemfile = expanded_gemfile_path
- @gemfiles << expanded_gemfile_path
- contents ||= Bundler.read_file(@gemfile.to_s)
- instance_eval(contents, @gemfile.to_s, 1)
- rescue Exception => e # rubocop:disable Lint/RescueException
- message = "There was an error " \
- "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \
- "`#{File.basename gemfile.to_s}`: #{e.message}"
-
- raise DSLError.new(message, gemfile, e.backtrace, contents)
- ensure
- @gemfile = original_gemfile
+ with_gemfile(gemfile) do |current_gemfile|
+ contents ||= Bundler.read_file(current_gemfile)
+ instance_eval(contents, current_gemfile, 1)
+ rescue GemfileEvalError => e
+ message = "There was an error evaluating `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ rescue GemfileError, InvalidArgumentError, InvalidOption, DeprecatedError, ScriptError => e
+ message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ rescue StandardError => e
+ raise unless e.backtrace_locations.first.path == current_gemfile
+ message = "There was an error parsing `#{File.basename current_gemfile}`: #{e.message}"
+ raise DSLError.new(message, current_gemfile, e.backtrace, contents)
+ end
end
def gemspec(opts = nil)
@@ -219,7 +219,7 @@ module Bundler
end
def github(repo, options = {})
- raise ArgumentError, "GitHub sources require a block" unless block_given?
+ raise InvalidArgumentError, "GitHub sources require a block" unless block_given?
github_uri = @git_sources["github"].call(repo)
git_options = normalize_hash(options).merge("uri" => github_uri)
git_source = @sources.add_git_source(git_options)
@@ -285,6 +285,16 @@ module Bundler
private
+ def with_gemfile(gemfile)
+ expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
+ original_gemfile = @gemfile
+ @gemfile = expanded_gemfile_path
+ @gemfiles << expanded_gemfile_path
+ yield @gemfile.to_s
+ ensure
+ @gemfile = original_gemfile
+ end
+
def add_git_sources
git_source(:github) do |repo_name|
if repo_name =~ GITHUB_PULL_REQUEST_URL
@@ -577,7 +587,7 @@ module Bundler
return m unless backtrace && dsl_path && contents
- trace_line = backtrace.find {|l| l.include?(dsl_path.to_s) } || trace_line
+ trace_line = backtrace.find {|l| l.include?(dsl_path) } || trace_line
return m unless trace_line
line_numer = trace_line.split(":")[1].to_i - 1
return m unless line_numer
@@ -603,7 +613,7 @@ module Bundler
def parse_line_number_from_description
description = self.description
- if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/
+ if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path}):\d+)/
trace_line = Regexp.last_match[1]
description = description.sub(/\n.*\n(\.\.\.)? *\^~+$/, "").sub(/#{Regexp.quote trace_line}:\s*/, "").sub("\n", " - ")
end
diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb
index 87cb352efa..201818cc33 100644
--- a/lib/bundler/endpoint_specification.rb
+++ b/lib/bundler/endpoint_specification.rb
@@ -92,6 +92,17 @@ module Bundler
end
end
+ # needed for `bundle fund`
+ def metadata
+ if @remote_specification
+ @remote_specification.metadata
+ elsif _local_specification
+ _local_specification.metadata
+ else
+ super
+ end
+ end
+
def _local_specification
return unless @loaded_from && File.exist?(local_specification_path)
eval(File.read(local_specification_path), nil, local_specification_path).tap do |spec|
diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb
index f6cb198e38..074bef6edc 100644
--- a/lib/bundler/env.rb
+++ b/lib/bundler/env.rb
@@ -120,7 +120,7 @@ module Bundler
specs = Bundler.rubygems.find_name(name)
out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty?
end
- if (exe = caller.last.split(":").first)&.match? %r{(exe|bin)/bundler?\z}
+ if (exe = caller_locations.last.absolute_path)&.match? %r{(exe|bin)/bundler?\z}
shebang = File.read(exe).lines.first
shebang.sub!(/^#!\s*/, "")
unless shebang.start_with?(Gem.ruby, "/usr/bin/env ruby")
diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb
index c29b1bfed8..a0ce739ad7 100644
--- a/lib/bundler/errors.rb
+++ b/lib/bundler/errors.rb
@@ -217,15 +217,15 @@ module Bundler
end
class InsecureInstallPathError < BundlerError
- def initialize(path)
+ def initialize(name, path)
+ @name = name
@path = path
end
def message
- "The installation path is insecure. Bundler cannot continue.\n" \
- "#{@path} is world-writable (without sticky bit).\n" \
- "Bundler cannot safely replace gems in world-writeable directories due to potential vulnerabilities.\n" \
- "Please change the permissions of this directory or choose a different install path."
+ "Bundler cannot reinstall #{@name} because there's a previous installation of it at #{@path} that is unsafe to remove.\n" \
+ "The parent of #{@path} is world-writable and does not have the sticky bit set, making it insecure to remove due to potential vulnerabilities.\n" \
+ "Please change the permissions of #{File.dirname(@path)} or choose a different install path."
end
status_code(38)
@@ -244,4 +244,6 @@ module Bundler
status_code(39)
end
+
+ class InvalidArgumentError < BundlerError; status_code(40); end
end
diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb
index 6288b22dcd..14721623f9 100644
--- a/lib/bundler/fetcher.rb
+++ b/lib/bundler/fetcher.rb
@@ -3,7 +3,7 @@
require_relative "vendored_persistent"
require_relative "vendored_timeout"
require "cgi"
-require "securerandom"
+require_relative "vendored_securerandom"
require "zlib"
module Bundler
@@ -182,7 +182,7 @@ module Bundler
agent << " ci/#{cis.join(",")}" if cis.any?
# add a random ID so we can consolidate runs server-side
- agent << " " << SecureRandom.hex(8)
+ agent << " " << Gem::SecureRandom.hex(8)
# add any user agent strings set in the config
extra_ua = Bundler.settings[:user_agent]
diff --git a/lib/bundler/fetcher/compact_index.rb b/lib/bundler/fetcher/compact_index.rb
index db914839b1..6e5656d26a 100644
--- a/lib/bundler/fetcher/compact_index.rb
+++ b/lib/bundler/fetcher/compact_index.rb
@@ -4,8 +4,6 @@ require_relative "base"
require_relative "../worker"
module Bundler
- autoload :CompactIndexClient, File.expand_path("../compact_index_client", __dir__)
-
class Fetcher
class CompactIndex < Base
def self.compact_index_request(method_name)
@@ -36,15 +34,8 @@ module Bundler
until remaining_gems.empty?
log_specs { "Looking up gems #{remaining_gems.inspect}" }
-
- deps = begin
- parallel_compact_index_client.dependencies(remaining_gems)
- rescue TooManyRequestsError
- @bundle_worker&.stop
- @bundle_worker = nil # reset it. Not sure if necessary
- serial_compact_index_client.dependencies(remaining_gems)
- end
- next_gems = deps.flat_map {|d| d[3].flat_map(&:first) }.uniq
+ deps = fetch_gem_infos(remaining_gems).flatten(1)
+ next_gems = deps.flat_map {|d| d[CompactIndexClient::INFO_DEPS].flat_map(&:first) }.uniq
deps.each {|dep| gem_info << dep }
complete_gems.concat(deps.map(&:first)).uniq!
remaining_gems = next_gems - complete_gems
@@ -61,7 +52,7 @@ module Bundler
return nil
end
# Read info file checksums out of /versions, so we can know if gems are up to date
- compact_index_client.update_and_parse_checksums!
+ compact_index_client.available?
rescue CompactIndexClient::Updater::MismatchedChecksumError => e
Bundler.ui.debug(e.message)
nil
@@ -81,20 +72,20 @@ module Bundler
end
end
- def parallel_compact_index_client
- compact_index_client.execution_mode = lambda do |inputs, &blk|
- func = lambda {|object, _index| blk.call(object) }
- worker = bundle_worker(func)
- inputs.each {|input| worker.enq(input) }
- inputs.map { worker.deq }
- end
-
- compact_index_client
+ def fetch_gem_infos(names)
+ in_parallel(names) {|name| compact_index_client.info(name) }
+ rescue TooManyRequestsError # rubygems.org is rate limiting us, slow down.
+ @bundle_worker&.stop
+ @bundle_worker = nil # reset it. Not sure if necessary
+ compact_index_client.reset!
+ names.map {|name| compact_index_client.info(name) }
end
- def serial_compact_index_client
- compact_index_client.sequential_execution_mode!
- compact_index_client
+ def in_parallel(inputs, &blk)
+ func = lambda {|object, _index| blk.call(object) }
+ worker = bundle_worker(func)
+ inputs.each {|input| worker.enq(input) }
+ inputs.map { worker.deq }
end
def bundle_worker(func = nil)
diff --git a/lib/bundler/force_platform.rb b/lib/bundler/force_platform.rb
index 249a24ecd1..7af33218cb 100644
--- a/lib/bundler/force_platform.rb
+++ b/lib/bundler/force_platform.rb
@@ -2,8 +2,6 @@
module Bundler
module ForcePlatform
- private
-
# The `:force_ruby_platform` value used by dependencies for resolution, and
# by locked specifications for materialization is `false` by default, except
# for TruffleRuby. TruffleRuby generally needs to force the RUBY platform
diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb
index d535d54f9b..5ce0ef6280 100644
--- a/lib/bundler/gem_helper.rb
+++ b/lib/bundler/gem_helper.rb
@@ -47,7 +47,7 @@ module Bundler
built_gem_path = build_gem
end
- desc "Generate SHA512 checksum if #{name}-#{version}.gem into the checksums directory."
+ desc "Generate SHA512 checksum of #{name}-#{version}.gem into the checksums directory."
task "build:checksum" => "build" do
build_checksum(built_gem_path)
end
diff --git a/lib/bundler/gem_helpers.rb b/lib/bundler/gem_helpers.rb
index de007523ec..70ccceb491 100644
--- a/lib/bundler/gem_helpers.rb
+++ b/lib/bundler/gem_helpers.rb
@@ -46,19 +46,26 @@ module Bundler
end
module_function :platform_specificity_match
- def select_best_platform_match(specs, platform)
- matching = specs.select {|spec| spec.match_platform(platform) }
+ def select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
+ matching = if force_ruby
+ specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! }
+ else
+ specs.select {|spec| spec.match_platform(platform) }
+ end
+
+ if prefer_locked
+ locked_originally = matching.select {|spec| spec.is_a?(LazySpecification) }
+ return locked_originally if locked_originally.any?
+ end
sort_best_platform_match(matching, platform)
end
module_function :select_best_platform_match
- def force_ruby_platform(specs)
- matching = specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! }
-
- sort_best_platform_match(matching, Gem::Platform::RUBY)
+ def select_best_local_platform_match(specs, force_ruby: false)
+ select_best_platform_match(specs, local_platform, force_ruby: force_ruby).map(&:materialize_for_installation).compact
end
- module_function :force_ruby_platform
+ module_function :select_best_local_platform_match
def sort_best_platform_match(matching, platform)
exact = matching.select {|spec| spec.platform == platform }
diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb
index 879b481339..c7e93c9ee0 100644
--- a/lib/bundler/injector.rb
+++ b/lib/bundler/injector.rb
@@ -23,10 +23,7 @@ module Bundler
# @param [Pathname] lockfile_path The lockfile in which to inject the new dependency.
# @return [Array]
def inject(gemfile_path, lockfile_path)
- if Bundler.frozen_bundle?
- # ensure the lock and Gemfile are synced
- Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true)
- end
+ Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true)
# temporarily unfreeze
Bundler.settings.temporary(deployment: false, frozen: false) do
diff --git a/lib/bundler/inline.rb b/lib/bundler/inline.rb
index ae4ccf2138..1b12de1d7c 100644
--- a/lib/bundler/inline.rb
+++ b/lib/bundler/inline.rb
@@ -46,11 +46,9 @@ def gemfile(install = false, options = {}, &gemfile)
Bundler::Plugin.gemfile_install(&gemfile) if Bundler.feature_flag.plugins?
builder = Bundler::Dsl.new
builder.instance_eval(&gemfile)
- builder.check_primary_source_safety
Bundler.settings.temporary(deployment: false, frozen: false) do
definition = builder.to_definition(nil, true)
- def definition.lock(*); end
definition.validate_runtime!
if install || definition.missing_specs?
@@ -62,8 +60,25 @@ def gemfile(install = false, options = {}, &gemfile)
end
end
- runtime = Bundler::Runtime.new(nil, definition)
- runtime.setup.require
+ begin
+ runtime = Bundler::Runtime.new(nil, definition).setup
+ rescue Gem::LoadError => e
+ name = e.name
+ version = e.requirement.requirements.first[1]
+ activated_version = Gem.loaded_specs[name].version
+
+ Bundler.ui.info \
+ "The #{name} gem was resolved to #{version}, but #{activated_version} was activated by Bundler while installing it, causing a conflict. " \
+ "Bundler will now retry resolving with #{activated_version} instead."
+
+ builder.dependencies.delete_if {|d| d.name == name }
+ builder.instance_eval { gem name, activated_version }
+ definition = builder.to_definition(nil, true)
+
+ retry
+ end
+
+ runtime.require
end
end
diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb
index 72e5602cc3..b65546a10a 100644
--- a/lib/bundler/installer.rb
+++ b/lib/bundler/installer.rb
@@ -69,9 +69,7 @@ module Bundler
Bundler.create_bundle_path
ProcessLock.lock do
- if Bundler.frozen_bundle?
- @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])
- end
+ @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])
if @definition.dependencies.empty?
Bundler.ui.warn "The Gemfile specifies no dependencies"
@@ -196,9 +194,14 @@ module Bundler
# that said, it's a rare situation (other than rake), and parallel
# installation is SO MUCH FASTER. so we let people opt in.
def install(options)
- force = options["force"]
+ standalone = options[:standalone]
+ force = options[:force]
+ local = options[:local]
jobs = installation_parallelization(options)
- install_in_parallel jobs, options[:standalone], force
+ spec_installations = ParallelInstaller.call(self, @definition.specs, jobs, standalone, force, local: local)
+ spec_installations.each do |installation|
+ post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message?
+ end
end
def installation_parallelization(options)
@@ -226,24 +229,17 @@ module Bundler
end
end
- def install_in_parallel(size, standalone, force = false)
- spec_installations = ParallelInstaller.call(self, @definition.specs, size, standalone, force)
- spec_installations.each do |installation|
- post_install_messages[installation.name] = installation.post_install_message if installation.has_post_install_message?
- end
- end
-
# returns whether or not a re-resolve was needed
def resolve_if_needed(options)
- @definition.resolution_mode = options
-
- if !@definition.unlocking? && !options["force"] && !Bundler.settings[:inline] && Bundler.default_lockfile.file?
- return false if @definition.nothing_changed? && !@definition.missing_specs?
+ @definition.prefer_local! if options[:"prefer-local"]
+
+ if options[:local] || (@definition.no_resolve_needed? && !@definition.missing_specs?)
+ @definition.resolve_with_cache!
+ false
+ else
+ @definition.resolve_remotely!
+ true
end
-
- @definition.setup_sources_for_resolve
-
- true
end
def lock
diff --git a/lib/bundler/installer/gem_installer.rb b/lib/bundler/installer/gem_installer.rb
index d3bbcc90f5..1da91857bd 100644
--- a/lib/bundler/installer/gem_installer.rb
+++ b/lib/bundler/installer/gem_installer.rb
@@ -2,14 +2,15 @@
module Bundler
class GemInstaller
- attr_reader :spec, :standalone, :worker, :force, :installer
+ attr_reader :spec, :standalone, :worker, :force, :local, :installer
- def initialize(spec, installer, standalone = false, worker = 0, force = false)
+ def initialize(spec, installer, standalone = false, worker = 0, force = false, local = false)
@spec = spec
@installer = installer
@standalone = standalone
@worker = worker
@force = force
+ @local = local
end
def install_from_spec
@@ -54,7 +55,7 @@ module Bundler
spec.source.install(
spec,
force: force,
- ensure_builtin_gems_cached: standalone,
+ local: local,
build_args: Array(spec_settings),
previous_spec: previous_spec,
)
diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb
index e745088f81..d10e5ec924 100644
--- a/lib/bundler/installer/parallel_installer.rb
+++ b/lib/bundler/installer/parallel_installer.rb
@@ -68,11 +68,12 @@ module Bundler
attr_reader :size
- def initialize(installer, all_specs, size, standalone, force, skip: nil)
+ def initialize(installer, all_specs, size, standalone, force, local: false, skip: nil)
@installer = installer
@size = size
@standalone = standalone
@force = force
+ @local = local
@specs = all_specs.map {|s| SpecInstallation.new(s) }
@specs.each do |spec_install|
spec_install.state = :installed if skip.include?(spec_install.name)
@@ -127,7 +128,7 @@ module Bundler
def do_install(spec_install, worker_num)
Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install)
gem_installer = Bundler::GemInstaller.new(
- spec_install.spec, @installer, @standalone, worker_num, @force
+ spec_install.spec, @installer, @standalone, worker_num, @force, @local
)
success, message = gem_installer.install_from_spec
if success
diff --git a/lib/bundler/installer/standalone.rb b/lib/bundler/installer/standalone.rb
index 5331df2e95..cf5993448c 100644
--- a/lib/bundler/installer/standalone.rb
+++ b/lib/bundler/installer/standalone.rb
@@ -58,9 +58,6 @@ module Bundler
else
SharedHelpers.relative_path_to(full_path, from: Bundler.root.join(bundler_path))
end
- rescue TypeError
- error_message = "#{spec.name} #{spec.version} has an invalid gemspec"
- raise Gem::InvalidSpecificationException.new(error_message)
end
def prevent_gem_activation
diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb
index 1e11621e55..8a15e356c4 100644
--- a/lib/bundler/lockfile_parser.rb
+++ b/lib/bundler/lockfile_parser.rb
@@ -272,7 +272,7 @@ module Bundler
end
def parse_platform(line)
- @platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/
+ @platforms << Gem::Platform.new($1.strip) if line =~ /^ (.*)$/
end
def parse_bundled_with(line)
diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1
index 56a3b6f85c..dae05bd945 100644
--- a/lib/bundler/man/bundle-add.1
+++ b/lib/bundler/man/bundle-add.1
@@ -1,24 +1,12 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-ADD" "1" "May 2024" ""
+.TH "BUNDLE\-ADD" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install
.SH "SYNOPSIS"
-\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT] [\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-skip\-install] [\-\-strict] [\-\-optimistic]
+\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
.SH "DESCRIPTION"
-Adds the named gem to the Gemfile and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\.
-.P
-Example:
-.P
-bundle add rails
-.P
-bundle add rails \-\-version "< 3\.0, > 1\.1"
-.P
-bundle add rails \-\-version "~> 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development"
-.P
-bundle add rails \-\-skip\-install
-.P
-bundle add rails \-\-group "development, test"
+Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\.
.SH "OPTIONS"
.TP
\fB\-\-version\fR, \fB\-v\fR
@@ -56,4 +44,27 @@ Adds optimistic declaration of version\.
.TP
\fB\-\-strict\fR
Adds strict declaration of version\.
-
+.SH "EXAMPLES"
+.IP "1." 4
+You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\.
+.IP
+\fBbundle add rails\fR
+.IP "2." 4
+You can add the \fBrails\fR gem with version greater than 1\.1 (not including 1\.1) and less than 3\.0\.
+.IP
+\fBbundle add rails \-\-version "> 1\.1, < 3\.0"\fR
+.IP "3." 4
+You can use the \fBhttps://gems\.example\.com\fR custom source and assign the gem to a group\.
+.IP
+\fBbundle add rails \-\-version "~> 5\.0\.0" \-\-source "https://gems\.example\.com" \-\-group "development"\fR
+.IP "4." 4
+The following adds the \fBgem\fR entry to the Gemfile without installing the gem\. You can install gems later via \fBbundle install\fR\.
+.IP
+\fBbundle add rails \-\-skip\-install\fR
+.IP "5." 4
+You can assign the gem to more than one group\.
+.IP
+\fBbundle add rails \-\-group "development, test"\fR
+.IP "" 0
+.SH "SEE ALSO"
+Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR
diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn
index 37c92e5fcd..8b38c7a248 100644
--- a/lib/bundler/man/bundle-add.1.ronn
+++ b/lib/bundler/man/bundle-add.1.ronn
@@ -1,26 +1,19 @@
bundle-add(1) -- Add gem to the Gemfile and run bundle install
-================================================================
+==============================================================
## SYNOPSIS
-`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE] [--path=PATH] [--git=GIT] [--github=GITHUB] [--branch=BRANCH] [--ref=REF] [--skip-install] [--strict] [--optimistic]
+`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE]
+ [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF]
+ [--skip-install] [--strict|--optimistic]
## DESCRIPTION
-Adds the named gem to the Gemfile and run `bundle install`. `bundle install` can be avoided by using the flag `--skip-install`.
-Example:
-
-bundle add rails
-
-bundle add rails --version "< 3.0, > 1.1"
-
-bundle add rails --version "~> 5.0.0" --source "https://gems.example.com" --group "development"
-
-bundle add rails --skip-install
-
-bundle add rails --group "development, test"
+Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`.
+`bundle install` can be avoided by using the flag `--skip-install`.
## OPTIONS
+
* `--version`, `-v`:
Specify version requirements(s) for the added gem.
@@ -56,3 +49,33 @@ bundle add rails --group "development, test"
* `--strict`:
Adds strict declaration of version.
+
+## EXAMPLES
+
+1. You can add the `rails` gem to the Gemfile without any version restriction.
+ The source of the gem will be the global source.
+
+ `bundle add rails`
+
+2. You can add the `rails` gem with version greater than 1.1 (not including 1.1) and less than 3.0.
+
+ `bundle add rails --version "> 1.1, < 3.0"`
+
+3. You can use the `https://gems.example.com` custom source and assign the gem
+ to a group.
+
+ `bundle add rails --version "~> 5.0.0" --source "https://gems.example.com" --group "development"`
+
+4. The following adds the `gem` entry to the Gemfile without installing the
+ gem. You can install gems later via `bundle install`.
+
+ `bundle add rails --skip-install`
+
+5. You can assign the gem to more than one group.
+
+ `bundle add rails --group "development, test"`
+
+## SEE ALSO
+
+[Gemfile(5)](https://bundler.io/man/gemfile.5.html),
+[bundle-remove(1)](bundle-remove.1.html)
diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1
index 4ec301951f..56c9966e75 100644
--- a/lib/bundler/man/bundle-binstubs.1
+++ b/lib/bundler/man/bundle-binstubs.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-BINSTUBS" "1" "May 2024" ""
+.TH "BUNDLE\-BINSTUBS" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-binstubs\fR \- Install the binstubs of the listed gems
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1
index e2da1269e6..d634eef203 100644
--- a/lib/bundler/man/bundle-cache.1
+++ b/lib/bundler/man/bundle-cache.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-CACHE" "1" "May 2024" ""
+.TH "BUNDLE\-CACHE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1
index dee1af1326..e15a41e4fd 100644
--- a/lib/bundler/man/bundle-check.1
+++ b/lib/bundler/man/bundle-check.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-CHECK" "1" "May 2024" ""
+.TH "BUNDLE\-CHECK" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1
index 7c7f9b5c77..aa5ccf7594 100644
--- a/lib/bundler/man/bundle-clean.1
+++ b/lib/bundler/man/bundle-clean.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-CLEAN" "1" "May 2024" ""
+.TH "BUNDLE\-CLEAN" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1
index a207dbbcaa..47104fb5c6 100644
--- a/lib/bundler/man/bundle-config.1
+++ b/lib/bundler/man/bundle-config.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-CONFIG" "1" "May 2024" ""
+.TH "BUNDLE\-CONFIG" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-config\fR \- Set bundler configuration options
.SH "SYNOPSIS"
@@ -307,7 +307,7 @@ Any \fB\.\fR characters in a host name are mapped to a double underscore (\fB__\
.P
This means that if you have a gem server named \fBmy\.gem\-host\.com\fR, you'll need to use the \fBBUNDLE_MY__GEM___HOST__COM\fR variable to configure credentials for it through ENV\.
.SH "CONFIGURE BUNDLER DIRECTORIES"
-Bundler's home, config, cache and plugin directories are able to be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values
+Bundler's home, cache and plugin directories and config file can be configured through environment variables\. The default location for Bundler's home directory is \fB~/\.bundle\fR, which all directories inherit from by default\. The following outlines the available environment variables and their default values
.IP "" 4
.nf
BUNDLE_USER_HOME : $HOME/\.bundle
diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn
index 7e5f458fb2..1a0ec2a5dc 100644
--- a/lib/bundler/man/bundle-config.1.ronn
+++ b/lib/bundler/man/bundle-config.1.ronn
@@ -397,7 +397,7 @@ through ENV.
## CONFIGURE BUNDLER DIRECTORIES
-Bundler's home, config, cache and plugin directories are able to be configured
+Bundler's home, cache and plugin directories and config file can be configured
through environment variables. The default location for Bundler's home directory is
`~/.bundle`, which all directories inherit from by default. The following
outlines the available environment variables and their default values
diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1
index dca18ec43d..f2b2ddaed0 100644
--- a/lib/bundler/man/bundle-console.1
+++ b/lib/bundler/man/bundle-console.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-CONSOLE" "1" "May 2024" ""
+.TH "BUNDLE\-CONSOLE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-console\fR \- Deprecated way to open an IRB session with the bundle pre\-loaded
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1
index 6489cc07f7..f225d0cd79 100644
--- a/lib/bundler/man/bundle-doctor.1
+++ b/lib/bundler/man/bundle-doctor.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-DOCTOR" "1" "May 2024" ""
+.TH "BUNDLE\-DOCTOR" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-doctor\fR \- Checks the bundle for common problems
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1
index 1548d29670..e16b7bc747 100644
--- a/lib/bundler/man/bundle-exec.1
+++ b/lib/bundler/man/bundle-exec.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-EXEC" "1" "May 2024" ""
+.TH "BUNDLE\-EXEC" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-exec\fR \- Execute a command in the context of the bundle
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1
index 5df7b0ef2f..e6e58cd409 100644
--- a/lib/bundler/man/bundle-gem.1
+++ b/lib/bundler/man/bundle-gem.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-GEM" "1" "May 2024" ""
+.TH "BUNDLE\-GEM" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem
.SH "SYNOPSIS"
@@ -44,6 +44,8 @@ When Bundler is configured to not generate tests, an interactive prompt will be
.IP
When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
.IP "\(bu" 4
+\fB\-\-no\-test\fR: Do not use a test framework (overrides \fB\-\-test\fR specified in the global config)\.
+.IP "\(bu" 4
\fB\-\-ci\fR, \fB\-\-ci=github\fR, \fB\-\-ci=gitlab\fR, \fB\-\-ci=circle\fR: Specify the continuous integration service that Bundler should use when generating the project\. Acceptable values are \fBgithub\fR, \fBgitlab\fR and \fBcircle\fR\. A configuration file will be generated in the project directory\. Given no option is specified:
.IP
When Bundler is configured to generate CI files, this defaults to Bundler's global config setting \fBgem\.ci\fR\.
@@ -52,6 +54,8 @@ When Bundler is configured to not generate CI files, an interactive prompt will
.IP
When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
.IP "\(bu" 4
+\fB\-\-no\-ci\fR: Do not use a continuous integration service (overrides \fB\-\-ci\fR specified in the global config)\.
+.IP "\(bu" 4
\fB\-\-linter\fR, \fB\-\-linter=rubocop\fR, \fB\-\-linter=standard\fR: Specify the linter and code formatter that Bundler should add to the project's development dependencies\. Acceptable values are \fBrubocop\fR and \fBstandard\fR\. A configuration file will be generated in the project directory\. Given no option is specified:
.IP
When Bundler is configured to add a linter, this defaults to Bundler's global config setting \fBgem\.linter\fR\.
@@ -60,6 +64,8 @@ When Bundler is configured not to add a linter, an interactive prompt will be di
.IP
When Bundler is unconfigured, an interactive prompt will be displayed and the answer will be saved in Bundler's global config for future \fBbundle gem\fR use\.
.IP "\(bu" 4
+\fB\-\-no\-linter\fR: Do not add a linter (overrides \fB\-\-linter\fR specified in the global config)\.
+.IP "\(bu" 4
\fB\-e\fR, \fB\-\-edit[=EDITOR]\fR: Open the resulting GEM_NAME\.gemspec in EDITOR, or the default editor if not specified\. The default is \fB$BUNDLER_EDITOR\fR, \fB$VISUAL\fR, or \fB$EDITOR\fR\.
.IP "" 0
.SH "SEE ALSO"
diff --git a/lib/bundler/man/bundle-gem.1.ronn b/lib/bundler/man/bundle-gem.1.ronn
index 46fa2f179f..2d71d8dabe 100644
--- a/lib/bundler/man/bundle-gem.1.ronn
+++ b/lib/bundler/man/bundle-gem.1.ronn
@@ -76,6 +76,10 @@ configuration file using the following names:
the answer will be saved in Bundler's global config for future `bundle gem`
use.
+* `--no-test`:
+ Do not use a test framework (overrides `--test` specified in the global
+ config).
+
* `--ci`, `--ci=github`, `--ci=gitlab`, `--ci=circle`:
Specify the continuous integration service that Bundler should use when
generating the project. Acceptable values are `github`, `gitlab`
@@ -92,6 +96,10 @@ configuration file using the following names:
the answer will be saved in Bundler's global config for future `bundle gem`
use.
+* `--no-ci`:
+ Do not use a continuous integration service (overrides `--ci` specified in
+ the global config).
+
* `--linter`, `--linter=rubocop`, `--linter=standard`:
Specify the linter and code formatter that Bundler should add to the
project's development dependencies. Acceptable values are `rubocop` and
@@ -108,6 +116,9 @@ configuration file using the following names:
the answer will be saved in Bundler's global config for future `bundle gem`
use.
+* `--no-linter`:
+ Do not add a linter (overrides `--linter` specified in the global config).
+
* `-e`, `--edit[=EDITOR]`:
Open the resulting GEM_NAME.gemspec in EDITOR, or the default editor if not
specified. The default is `$BUNDLER_EDITOR`, `$VISUAL`, or `$EDITOR`.
diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1
index a3e7c7770d..d7a05f824e 100644
--- a/lib/bundler/man/bundle-help.1
+++ b/lib/bundler/man/bundle-help.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-HELP" "1" "May 2024" ""
+.TH "BUNDLE\-HELP" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-help\fR \- Displays detailed help for each subcommand
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1
index a3d7ff0988..6b401a57f4 100644
--- a/lib/bundler/man/bundle-info.1
+++ b/lib/bundler/man/bundle-info.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-INFO" "1" "May 2024" ""
+.TH "BUNDLE\-INFO" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-info\fR \- Show information for the given gem in your bundle
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1
index a0edaaa18f..f2e444c7c2 100644
--- a/lib/bundler/man/bundle-init.1
+++ b/lib/bundler/man/bundle-init.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-INIT" "1" "May 2024" ""
+.TH "BUNDLE\-INIT" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-init\fR \- Generates a Gemfile into the current working directory
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-inject.1 b/lib/bundler/man/bundle-inject.1
index 7a1038206e..8eb3633837 100644
--- a/lib/bundler/man/bundle-inject.1
+++ b/lib/bundler/man/bundle-inject.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-INJECT" "1" "May 2024" ""
+.TH "BUNDLE\-INJECT" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-inject\fR \- Add named gem(s) with version requirements to Gemfile
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1
index cc46a03b7f..7539d18f81 100644
--- a/lib/bundler/man/bundle-install.1
+++ b/lib/bundler/man/bundle-install.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-INSTALL" "1" "May 2024" ""
+.TH "BUNDLE\-INSTALL" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-install\fR \- Install the dependencies specified in your Gemfile
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1
index 21723608cc..5cbb1c3cfe 100644
--- a/lib/bundler/man/bundle-list.1
+++ b/lib/bundler/man/bundle-list.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-LIST" "1" "May 2024" ""
+.TH "BUNDLE\-LIST" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-list\fR \- List all the gems in the bundle
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1
index 8b81b7732f..5f0d43a9aa 100644
--- a/lib/bundler/man/bundle-lock.1
+++ b/lib/bundler/man/bundle-lock.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-LOCK" "1" "May 2024" ""
+.TH "BUNDLE\-LOCK" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-lock\fR \- Creates / Updates a lockfile without installing
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1
index 41a8804a09..fb5ff1fee7 100644
--- a/lib/bundler/man/bundle-open.1
+++ b/lib/bundler/man/bundle-open.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-OPEN" "1" "May 2024" ""
+.TH "BUNDLE\-OPEN" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-open\fR \- Opens the source directory for a gem in your bundle
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1
index 838fce45cd..ea3005dd87 100644
--- a/lib/bundler/man/bundle-outdated.1
+++ b/lib/bundler/man/bundle-outdated.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-OUTDATED" "1" "May 2024" ""
+.TH "BUNDLE\-OUTDATED" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-outdated\fR \- List installed gems with newer versions available
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1
index 0dbce7a7a4..c3058175fc 100644
--- a/lib/bundler/man/bundle-platform.1
+++ b/lib/bundler/man/bundle-platform.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-PLATFORM" "1" "May 2024" ""
+.TH "BUNDLE\-PLATFORM" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-platform\fR \- Displays platform compatibility information
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1
index 1290abb3ba..34437d9973 100644
--- a/lib/bundler/man/bundle-plugin.1
+++ b/lib/bundler/man/bundle-plugin.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-PLUGIN" "1" "May 2024" ""
+.TH "BUNDLE\-PLUGIN" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-plugin\fR \- Manage Bundler plugins
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1
index bf4a7cd323..103c6f68ae 100644
--- a/lib/bundler/man/bundle-pristine.1
+++ b/lib/bundler/man/bundle-pristine.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-PRISTINE" "1" "May 2024" ""
+.TH "BUNDLE\-PRISTINE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-pristine\fR \- Restores installed gems to their pristine condition
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1
index c3c96b416d..4a2ed4eb13 100644
--- a/lib/bundler/man/bundle-remove.1
+++ b/lib/bundler/man/bundle-remove.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-REMOVE" "1" "May 2024" ""
+.TH "BUNDLE\-REMOVE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-remove\fR \- Removes gems from the Gemfile
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1
index c054875efd..dfbb439218 100644
--- a/lib/bundler/man/bundle-show.1
+++ b/lib/bundler/man/bundle-show.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-SHOW" "1" "May 2024" ""
+.TH "BUNDLE\-SHOW" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1
index acd80ec75c..5eb9514f03 100644
--- a/lib/bundler/man/bundle-update.1
+++ b/lib/bundler/man/bundle-update.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-UPDATE" "1" "May 2024" ""
+.TH "BUNDLE\-UPDATE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-update\fR \- Update your gems to the latest available versions
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1
index 44eaf92224..a29858181a 100644
--- a/lib/bundler/man/bundle-version.1
+++ b/lib/bundler/man/bundle-version.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-VERSION" "1" "May 2024" ""
+.TH "BUNDLE\-VERSION" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-version\fR \- Prints Bundler version information
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle-viz.1 b/lib/bundler/man/bundle-viz.1
index 77b902214c..9609e098dd 100644
--- a/lib/bundler/man/bundle-viz.1
+++ b/lib/bundler/man/bundle-viz.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE\-VIZ" "1" "May 2024" ""
+.TH "BUNDLE\-VIZ" "1" "September 2024" ""
.SH "NAME"
\fBbundle\-viz\fR \- Generates a visual dependency graph for your Gemfile
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1
index 199d1ce9fd..d84d788748 100644
--- a/lib/bundler/man/bundle.1
+++ b/lib/bundler/man/bundle.1
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "BUNDLE" "1" "May 2024" ""
+.TH "BUNDLE" "1" "September 2024" ""
.SH "NAME"
\fBbundle\fR \- Ruby Dependency Management
.SH "SYNOPSIS"
diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5
index fa9e31f615..f24a1c540d 100644
--- a/lib/bundler/man/gemfile.5
+++ b/lib/bundler/man/gemfile.5
@@ -1,6 +1,6 @@
.\" generated with nRonn/v0.11.1
.\" https://github.com/n-ronn/nronn/tree/0.11.1
-.TH "GEMFILE" "5" "May 2024" ""
+.TH "GEMFILE" "5" "September 2024" ""
.SH "NAME"
\fBGemfile\fR \- A format for describing gem dependencies for Ruby programs
.SH "SYNOPSIS"
@@ -216,6 +216,8 @@ The following platform values are deprecated and should be replaced with \fBwind
.IP "\(bu" 4
\fBmswin\fR, \fBmswin64\fR, \fBmingw32\fR, \fBx64_mingw\fR
.IP "" 0
+.P
+Note that, while unfortunately using the same terminology, the values of this option are different from the values that \fBbundle lock \-\-add\-platform\fR can take\. The values of this option are more closer to "Ruby Implementation" while the values that \fBbundle lock \-\-add\-platform\fR understands are more related to OS and architecture of the different systems where your lockfile will be used\.
.SS "FORCE_RUBY_PLATFORM"
If you always want the pure ruby variant of a gem to be chosen over platform specific variants, you can use the \fBforce_ruby_platform\fR option:
.IP "" 4
diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn
index 7c1e00d13a..802549737e 100644
--- a/lib/bundler/man/gemfile.5.ronn
+++ b/lib/bundler/man/gemfile.5.ronn
@@ -242,6 +242,12 @@ The following platform values are deprecated and should be replaced with `window
* `mswin`, `mswin64`, `mingw32`, `x64_mingw`
+Note that, while unfortunately using the same terminology, the values of this
+option are different from the values that `bundle lock --add-platform` can take.
+The values of this option are more closer to "Ruby Implementation" while the
+values that `bundle lock --add-platform` understands are more related to OS and
+architecture of the different systems where your lockfile will be used.
+
### FORCE_RUBY_PLATFORM
If you always want the pure ruby variant of a gem to be chosen over platform
diff --git a/lib/bundler/plugin/api/source.rb b/lib/bundler/plugin/api/source.rb
index 8563ee358a..690f379389 100644
--- a/lib/bundler/plugin/api/source.rb
+++ b/lib/bundler/plugin/api/source.rb
@@ -131,7 +131,7 @@ module Bundler
Bundler::Index.build do |index|
files.each do |file|
next unless spec = Bundler.load_gemspec(file)
- Bundler.rubygems.set_installed_by_version(spec)
+ spec.installed_by_version = Gem::VERSION
spec.source = self
Bundler.rubygems.validate(spec)
@@ -196,6 +196,7 @@ module Bundler
FileUtils.rm_rf(new_cache_path)
FileUtils.cp_r(install_path, new_cache_path)
+ FileUtils.rm_rf(app_cache_path.join(".git"))
FileUtils.touch(app_cache_path.join(".bundlecache"))
end
diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb
index 1a6711ea6f..a38b6974f8 100644
--- a/lib/bundler/resolver.rb
+++ b/lib/bundler/resolver.rb
@@ -79,13 +79,14 @@ module Bundler
def solve_versions(root:, logger:)
solver = PubGrub::VersionSolver.new(source: self, root: root, logger: logger)
result = solver.solve
- result.map {|package, version| version.to_specs(package) }.flatten.uniq
+ resolved_specs = result.map {|package, version| version.to_specs(package) }.flatten
+ resolved_specs |= @base.specs_compatible_with(SpecSet.new(resolved_specs))
rescue PubGrub::SolveFailure => e
incompatibility = e.incompatibility
- names_to_unlock, names_to_allow_prereleases_for, extended_explanation = find_names_to_relax(incompatibility)
+ names_to_unlock, names_to_allow_prereleases_for, names_to_allow_remote_specs_for, extended_explanation = find_names_to_relax(incompatibility)
- names_to_relax = names_to_unlock + names_to_allow_prereleases_for
+ names_to_relax = names_to_unlock + names_to_allow_prereleases_for + names_to_allow_remote_specs_for
if names_to_relax.any?
if names_to_unlock.any?
@@ -95,11 +96,17 @@ module Bundler
end
if names_to_allow_prereleases_for.any?
- Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retrying considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true
+ Bundler.ui.debug "Found conflicts with dependencies with prereleases. Will retry considering prereleases for #{names_to_allow_prereleases_for.join(", ")}...", true
@base.include_prereleases(names_to_allow_prereleases_for)
end
+ if names_to_allow_remote_specs_for.any?
+ Bundler.ui.debug "Found conflicts with local versions of #{names_to_allow_remote_specs_for.join(", ")}. Will retry considering remote versions...", true
+
+ @base.include_remote_specs(names_to_allow_remote_specs_for)
+ end
+
root, logger = setup_solver
Bundler.ui.debug "Retrying resolution...", true
@@ -119,6 +126,7 @@ module Bundler
def find_names_to_relax(incompatibility)
names_to_unlock = []
names_to_allow_prereleases_for = []
+ names_to_allow_remote_specs_for = []
extended_explanation = nil
while incompatibility.conflict?
@@ -133,6 +141,8 @@ module Bundler
names_to_unlock << name
elsif package.ignores_prereleases? && @all_specs[name].any? {|s| s.version.prerelease? }
names_to_allow_prereleases_for << name
+ elsif package.prefer_local? && @all_specs[name].any? {|s| !s.is_a?(StubSpecification) }
+ names_to_allow_remote_specs_for << name
end
no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) }
@@ -142,7 +152,7 @@ module Bundler
end
end
- [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, extended_explanation]
+ [names_to_unlock.uniq, names_to_allow_prereleases_for.uniq, names_to_allow_remote_specs_for.uniq, extended_explanation]
end
def parse_dependency(package, dependency)
@@ -243,7 +253,7 @@ module Bundler
def all_versions_for(package)
name = package.name
- results = (@base[name] + filter_prereleases(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] }
+ results = (@base[name] + filter_specs(@all_specs[name], package)).uniq {|spec| [spec.version.hash, spec.platform] }
if name == "bundler" && !bundler_pinned_to_current_version?
bundler_spec = Gem.loaded_specs["bundler"]
@@ -254,7 +264,7 @@ module Bundler
results = filter_matching_specs(results, locked_requirement) if locked_requirement
results.group_by(&:version).reduce([]) do |groups, (version, specs)|
- platform_specs = package.platforms.map {|platform| select_best_platform_match(specs, platform) }
+ platform_specs = package.platform_specs(specs)
# If package is a top-level dependency,
# candidate is only valid if there are matching versions for all resolution platforms.
@@ -269,14 +279,22 @@ module Bundler
next groups if platform_specs.all?(&:empty?)
end
- platform_specs.flatten!
-
ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY)
- groups << Resolver::Candidate.new(version, specs: ruby_specs) if ruby_specs.any?
+ ruby_group = Resolver::SpecGroup.new(ruby_specs)
+
+ unless ruby_group.empty?
+ platform_specs.each do |specs|
+ ruby_group.merge(Resolver::SpecGroup.new(specs))
+ end
+
+ groups << Resolver::Candidate.new(version, group: ruby_group, priority: -1)
+ next groups if package.force_ruby_platform?
+ end
- next groups if platform_specs == ruby_specs || package.force_ruby_platform?
+ platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq)
+ next groups if platform_group == ruby_group
- groups << Resolver::Candidate.new(version, specs: platform_specs)
+ groups << Resolver::Candidate.new(version, group: platform_group, priority: 1)
groups
end
@@ -359,12 +377,22 @@ module Bundler
end
end
+ def filter_specs(specs, package)
+ filter_remote_specs(filter_prereleases(specs, package), package)
+ end
+
def filter_prereleases(specs, package)
return specs unless package.ignores_prereleases? && specs.size > 1
specs.reject {|s| s.version.prerelease? }
end
+ def filter_remote_specs(specs, package)
+ return specs unless package.prefer_local?
+
+ specs.select {|s| s.is_a?(StubSpecification) }
+ end
+
# Ignore versions that depend on themselves incorrectly
def filter_invalid_self_dependencies(specs, name)
specs.reject do |s|
@@ -396,10 +424,13 @@ module Bundler
dep_range = dep_constraint.range
versions = select_sorted_versions(dep_package, dep_range)
- if versions.empty? && dep_package.ignores_prereleases?
- @all_versions.delete(dep_package)
- @sorted_versions.delete(dep_package)
- dep_package.consider_prereleases!
+ if versions.empty?
+ if dep_package.ignores_prereleases? || dep_package.prefer_local?
+ @all_versions.delete(dep_package)
+ @sorted_versions.delete(dep_package)
+ end
+ dep_package.consider_prereleases! if dep_package.ignores_prereleases?
+ dep_package.consider_remote_versions! if dep_package.prefer_local?
versions = select_sorted_versions(dep_package, dep_range)
end
@@ -431,8 +462,8 @@ module Bundler
def requirement_to_range(requirement)
ranges = requirement.requirements.map do |(op, version)|
- ver = Resolver::Candidate.new(version).generic!
- platform_ver = Resolver::Candidate.new(version).platform_specific!
+ ver = Resolver::Candidate.new(version, priority: -1)
+ platform_ver = Resolver::Candidate.new(version, priority: 1)
case op
when "~>"
diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb
index ad19eeb3f4..3f2436672a 100644
--- a/lib/bundler/resolver/base.rb
+++ b/lib/bundler/resolver/base.rb
@@ -30,6 +30,10 @@ module Bundler
end.compact
end
+ def specs_compatible_with(result)
+ @base.specs_compatible_with(result)
+ end
+
def [](name)
@base[name]
end
@@ -68,6 +72,12 @@ module Bundler
end
end
+ def include_remote_specs(names)
+ names.each do |name|
+ get_package(name).consider_remote_versions!
+ end
+ end
+
private
def indirect_pins(names)
diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb
index 9e8b913335..f593fc5d61 100644
--- a/lib/bundler/resolver/candidate.rb
+++ b/lib/bundler/resolver/candidate.rb
@@ -24,10 +24,10 @@ module Bundler
attr_reader :version
- def initialize(version, specs: [])
- @spec_group = Resolver::SpecGroup.new(specs)
+ def initialize(version, group: nil, priority: -1)
+ @spec_group = group || SpecGroup.new([])
@version = Gem::Version.new(version)
- @ruby_only = specs.map(&:platform).uniq == [Gem::Platform::RUBY]
+ @priority = priority
end
def dependencies
@@ -40,18 +40,6 @@ module Bundler
@spec_group.to_specs(package.force_ruby_platform?)
end
- def generic!
- @ruby_only = true
-
- self
- end
-
- def platform_specific!
- @ruby_only = false
-
- self
- end
-
def prerelease?
@version.prerelease?
end
@@ -61,7 +49,7 @@ module Bundler
end
def sort_obj
- [@version, @ruby_only ? -1 : 1]
+ [@version, @priority]
end
def <=>(other)
diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb
index 0461328683..5aecc12d05 100644
--- a/lib/bundler/resolver/package.rb
+++ b/lib/bundler/resolver/package.rb
@@ -15,7 +15,7 @@ module Bundler
class Package
attr_reader :name, :platforms, :dependency, :locked_version
- def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, dependency: nil)
+ def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil)
@name = name
@platforms = platforms
@locked_version = locked_specs[name].first&.version
@@ -23,6 +23,11 @@ module Bundler
@dependency = dependency || Dependency.new(name, @locked_version)
@top_level = !dependency.nil?
@prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
+ @prefer_local = prefer_local
+ end
+
+ def platform_specs(specs)
+ platforms.map {|platform| GemHelpers.select_best_platform_match(specs, platform, prefer_locked: !unlock?) }
end
def to_s
@@ -65,6 +70,14 @@ module Bundler
@prerelease = :consider_last
end
+ def prefer_local?
+ @prefer_local
+ end
+
+ def consider_remote_versions!
+ @prefer_local = false
+ end
+
def force_ruby_platform?
@dependency.force_ruby_platform
end
diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb
index 5cee444e5e..f950df6eda 100644
--- a/lib/bundler/resolver/spec_group.rb
+++ b/lib/bundler/resolver/spec_group.rb
@@ -3,6 +3,8 @@
module Bundler
class Resolver
class SpecGroup
+ attr_reader :specs
+
def initialize(specs)
@specs = specs
end
@@ -38,17 +40,33 @@ module Bundler
def dependencies
@dependencies ||= @specs.map do |spec|
__dependencies(spec) + metadata_dependencies(spec)
- end.flatten.uniq
+ end.flatten.uniq.sort
+ end
+
+ def ==(other)
+ sorted_spec_names == other.sorted_spec_names
+ end
+
+ def merge(other)
+ return false unless equivalent?(other)
+
+ @specs |= other.specs
+
+ true
end
protected
def sorted_spec_names
- @sorted_spec_names ||= @specs.map(&:full_name).sort
+ @specs.map(&:full_name).sort
end
private
+ def equivalent?(other)
+ name == other.name && version == other.version && source == other.source && dependencies == other.dependencies
+ end
+
def exemplary_spec
@specs.first
end
diff --git a/lib/bundler/retry.rb b/lib/bundler/retry.rb
index b95c42c361..090cb7e2ca 100644
--- a/lib/bundler/retry.rb
+++ b/lib/bundler/retry.rb
@@ -50,7 +50,7 @@ module Bundler
end
return true unless name
Bundler.ui.info "" unless Bundler.ui.debug? # Add new line in case dots preceded this
- Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", Bundler.ui.debug?
+ Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.class} #{e.message}", true
end
def keep_trying?
diff --git a/lib/bundler/ruby_version.rb b/lib/bundler/ruby_version.rb
index 7e9e072b83..0ed5cbc6ca 100644
--- a/lib/bundler/ruby_version.rb
+++ b/lib/bundler/ruby_version.rb
@@ -23,7 +23,13 @@ module Bundler
# specified must match the version.
@versions = Array(versions).map do |v|
- op, v = Gem::Requirement.parse(normalize_version(v))
+ normalized_v = normalize_version(v)
+
+ unless Gem::Requirement::PATTERN.match?(normalized_v)
+ raise InvalidArgumentError, "#{v} is not a valid requirement on the Ruby version"
+ end
+
+ op, v = Gem::Requirement.parse(normalized_v)
op == "=" ? v.to_s : "#{op} #{v}"
end
diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb
index 14b8703213..0f0680560a 100644
--- a/lib/bundler/rubygems_ext.rb
+++ b/lib/bundler/rubygems_ext.rb
@@ -23,9 +23,57 @@ unless Gem.ruby_version.to_s == RUBY_VERSION || RUBY_PATCHLEVEL == -1
end
module Gem
+ # Can be removed once RubyGems 3.5.11 support is dropped
+ unless Gem.respond_to?(:freebsd_platform?)
+ def self.freebsd_platform?
+ RbConfig::CONFIG["host_os"].to_s.include?("bsd")
+ end
+ end
+
+ # Can be removed once RubyGems 3.5.18 support is dropped
+ unless Gem.respond_to?(:open_file_with_lock)
+ class << self
+ remove_method :open_file_with_flock if Gem.respond_to?(:open_file_with_flock)
+
+ def open_file_with_flock(path, &block)
+ mode = IO::RDONLY | IO::APPEND | IO::CREAT | IO::BINARY
+ mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE)
+
+ File.open(path, mode) do |io|
+ begin
+ io.flock(File::LOCK_EX)
+ rescue Errno::ENOSYS, Errno::ENOTSUP
+ rescue Errno::ENOLCK # NFS
+ raise unless Thread.main == Thread.current
+ end
+ yield io
+ end
+ end
+
+ def open_file_with_lock(path, &block)
+ file_lock = "#{path}.lock"
+ open_file_with_flock(file_lock, &block)
+ ensure
+ FileUtils.rm_f file_lock
+ end
+ end
+ end
+
require "rubygems/specification"
+ # Can be removed once RubyGems 3.5.14 support is dropped
+ VALIDATES_FOR_RESOLUTION = Specification.new.respond_to?(:validate_for_resolution).freeze
+
+ # Can be removed once RubyGems 3.3.15 support is dropped
+ FLATTENS_REQUIRED_PATHS = Specification.new.respond_to?(:flatten_require_paths).freeze
+
class Specification
+ # Can be removed once RubyGems 3.5.15 support is dropped
+ correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
+ unless @@array_attributes == correct_array_attributes
+ @@array_attributes = correct_array_attributes # rubocop:disable Style/ClassVars
+ end
+
require_relative "match_metadata"
require_relative "match_platform"
@@ -75,11 +123,6 @@ module Gem
end
end
- remove_method :gem_dir
- def gem_dir
- full_gem_path
- end
-
unless const_defined?(:LATEST_RUBY_WITHOUT_PATCH_VERSIONS)
LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1")
@@ -124,6 +167,33 @@ module Gem
!default_gem? && !File.directory?(full_gem_path)
end
+ unless VALIDATES_FOR_RESOLUTION
+ def validate_for_resolution
+ SpecificationPolicy.new(self).validate_for_resolution
+ end
+ end
+
+ unless FLATTENS_REQUIRED_PATHS
+ def flatten_require_paths
+ return unless raw_require_paths.first.is_a?(Array)
+
+ warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this"
+ raw_require_paths.flatten!
+ end
+
+ class << self
+ module RequirePathFlattener
+ def from_yaml(input)
+ spec = super(input)
+ spec.flatten_require_paths
+ spec
+ end
+ end
+
+ prepend RequirePathFlattener
+ end
+ end
+
private
def dependencies_to_gemfile(dependencies, group = nil)
@@ -143,6 +213,14 @@ module Gem
end
end
+ unless VALIDATES_FOR_RESOLUTION
+ class SpecificationPolicy
+ def validate_for_resolution
+ validate_required!
+ end
+ end
+ end
+
module BetterPermissionError
def data
super
@@ -162,26 +240,20 @@ module Gem
include ::Bundler::ForcePlatform
+ attr_reader :force_ruby_platform
+
attr_accessor :source, :groups
alias_method :eql?, :==
- def force_ruby_platform
- return @force_ruby_platform if defined?(@force_ruby_platform) && !@force_ruby_platform.nil?
-
- @force_ruby_platform = default_force_ruby_platform
- end
-
- def encode_with(coder)
- to_yaml_properties.each do |ivar|
- coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar)
+ unless method_defined?(:encode_with, false)
+ def encode_with(coder)
+ [:@name, :@requirement, :@type, :@prerelease, :@version_requirements].each do |ivar|
+ coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar)
+ end
end
end
- def to_yaml_properties
- instance_variables.reject {|p| ["@source", "@groups"].include?(p.to_s) }
- end
-
def to_lock
out = String.new(" #{name}")
unless requirement.none?
@@ -234,7 +306,7 @@ module Gem
# cpu
([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu ||
- (@cpu == "arm" && other.cpu.start_with?("arm"))) &&
+ (@cpu == "arm" && other.cpu.start_with?("armv"))) &&
# os
@os == other.os &&
@@ -338,4 +410,23 @@ module Gem
end
end
end
+
+ unless Gem.rubygems_version >= Gem::Version.new("3.5.19")
+ class Resolver::ActivationRequest
+ remove_method :installed?
+
+ def installed?
+ case @spec
+ when Gem::Resolver::VendorSpecification then
+ true
+ else
+ this_spec = full_spec
+
+ Gem::Specification.any? do |s|
+ s == this_spec && s.base_dir == this_spec.base_dir
+ end
+ end
+ end
+ end
+ end
end
diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb
index d563868cd2..62756680e7 100644
--- a/lib/bundler/rubygems_gem_installer.rb
+++ b/lib/bundler/rubygems_gem_installer.rb
@@ -29,7 +29,10 @@ module Bundler
write_build_info_file
run_post_build_hooks
- generate_bin
+ SharedHelpers.filesystem_access(bin_dir, :write) do
+ generate_bin
+ end
+
generate_plugins
write_spec
@@ -45,7 +48,17 @@ module Bundler
spec
end
- def pre_install_checks
+ if Bundler.rubygems.provides?("< 3.5")
+ def pre_install_checks
+ super
+ rescue Gem::FilePermissionError
+ # Ignore permission checks in RubyGems. Instead, go on, and try to write
+ # for real. We properly handle permission errors when they happen.
+ nil
+ end
+ end
+
+ def ensure_writable_dir(dir)
super
rescue Gem::FilePermissionError
# Ignore permission checks in RubyGems. Instead, go on, and try to write
@@ -68,6 +81,26 @@ module Bundler
end
end
+ if Bundler.rubygems.provides?("< 3.5.19")
+ def generate_bin_script(filename, bindir)
+ bin_script_path = File.join bindir, formatted_program_filename(filename)
+
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
+ FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
+
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
+ end
+ end
+
+ verbose bin_script_path
+
+ generate_windows_script filename, bindir
+ end
+ end
+
def build_extensions
extension_cache_path = options[:bundler_extension_cache_path]
extension_dir = spec.extension_dir
@@ -117,12 +150,13 @@ module Bundler
def strict_rm_rf(dir)
return unless File.exist?(dir)
+ return if Dir.empty?(dir)
parent = File.dirname(dir)
parent_st = File.stat(parent)
if parent_st.world_writable? && !parent_st.sticky?
- raise InsecureInstallPathError.new(parent)
+ raise InsecureInstallPathError.new(spec.full_name, dir)
end
begin
diff --git a/lib/bundler/rubygems_integration.rb b/lib/bundler/rubygems_integration.rb
index 6980b32236..b98d3b7b8c 100644
--- a/lib/bundler/rubygems_integration.rb
+++ b/lib/bundler/rubygems_integration.rb
@@ -4,7 +4,9 @@ require "rubygems" unless defined?(Gem)
module Bundler
class RubygemsIntegration
- autoload :Monitor, "monitor"
+ require "monitor"
+
+ EXT_LOCK = Monitor.new
def initialize
@replaced_methods = {}
@@ -32,6 +34,10 @@ module Bundler
Gem::Command.build_args = args
end
+ def set_target_rbconfig(path)
+ Gem.set_target_rbconfig(path)
+ end
+
def loaded_specs(name)
Gem.loaded_specs[name]
end
@@ -46,7 +52,7 @@ module Bundler
end
def validate(spec)
- Bundler.ui.silence { spec.validate(false) }
+ Bundler.ui.silence { spec.validate_for_resolution }
rescue Gem::InvalidSpecificationException => e
error_message = "The gemspec at #{spec.loaded_from} is not valid. Please fix this gemspec.\n" \
"The validation error was '#{e.message}'\n"
@@ -55,11 +61,6 @@ module Bundler
nil
end
- def set_installed_by_version(spec, installed_by_version = Gem::VERSION)
- return unless spec.respond_to?(:installed_by_version=)
- spec.installed_by_version = Gem::Version.create(installed_by_version)
- end
-
def spec_missing_extensions?(spec, default = true)
return spec.missing_extensions? if spec.respond_to?(:missing_extensions?)
@@ -171,7 +172,7 @@ module Bundler
end
def ext_lock
- @ext_lock ||= Monitor.new
+ EXT_LOCK
end
def spec_from_gem(path)
@@ -214,7 +215,7 @@ module Bundler
[::Kernel.singleton_class, ::Kernel].each do |kernel_class|
redefine_method(kernel_class, :gem) do |dep, *reqs|
- if executables&.include?(File.basename(caller.first.split(":").first))
+ if executables&.include?(File.basename(caller_locations(1, 1).first.path))
break
end
@@ -479,11 +480,25 @@ module Bundler
end
def all_specs
+ SharedHelpers.major_deprecation 2, "Bundler.rubygems.all_specs has been removed in favor of Bundler.rubygems.installed_specs"
+
Gem::Specification.stubs.map do |stub|
StubSpecification.from_stub(stub)
end
end
+ def installed_specs
+ Gem::Specification.stubs.reject(&:default_gem?).map do |stub|
+ StubSpecification.from_stub(stub)
+ end
+ end
+
+ def default_specs
+ Gem::Specification.default_stubs.map do |stub|
+ StubSpecification.from_stub(stub)
+ end
+ end
+
def find_bundler(version)
find_name("bundler").find {|s| s.version.to_s == version }
end
diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb
index 54aa30ce0b..9792a81962 100644
--- a/lib/bundler/runtime.rb
+++ b/lib/bundler/runtime.rb
@@ -10,7 +10,7 @@ module Bundler
end
def setup(*groups)
- @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.frozen_bundle?
+ @definition.ensure_equivalent_gemfile_and_lockfile
# Has to happen first
clean_load_path
@@ -139,11 +139,6 @@ module Bundler
spec.source.cache(spec, custom_path) if spec.source.respond_to?(:cache)
end
- Dir[cache_path.join("*/.git")].each do |git_dir|
- FileUtils.rm_rf(git_dir)
- FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
- end
-
prune_cache(cache_path) unless Bundler.settings[:no_prune]
end
diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb
index bfd000b1a0..ea7c014f3c 100644
--- a/lib/bundler/self_manager.rb
+++ b/lib/bundler/self_manager.rb
@@ -70,8 +70,23 @@ module Bundler
configured_gem_home = ENV["GEM_HOME"]
configured_gem_path = ENV["GEM_PATH"]
- cmd = [$PROGRAM_NAME, *ARGV]
- cmd.unshift(Gem.ruby) unless File.executable?($PROGRAM_NAME)
+ # Bundler specs need some stuff to be required before Bundler starts
+ # running, for example, for faking the compact index API. However, these
+ # flags are lost when we reexec to a different version of Bundler. In the
+ # future, we may be able to properly reconstruct the original Ruby
+ # invocation (see https://bugs.ruby-lang.org/issues/6648), but for now
+ # there's no way to do it, so we need to be explicit about how to re-exec.
+ # This may be a feature end users request at some point, but maybe by that
+ # time, we have builtin tools to do. So for now, we use an undocumented
+ # ENV variable only for our specs.
+ bundler_spec_original_cmd = ENV["BUNDLER_SPEC_ORIGINAL_CMD"]
+ if bundler_spec_original_cmd
+ require "shellwords"
+ cmd = [*Shellwords.shellsplit(bundler_spec_original_cmd), *ARGV]
+ else
+ cmd = [$PROGRAM_NAME, *ARGV]
+ cmd.unshift(Gem.ruby) unless File.executable?($PROGRAM_NAME)
+ end
Bundler.with_original_env do
Kernel.exec(
@@ -92,6 +107,7 @@ module Bundler
def autoswitching_applies?
ENV["BUNDLER_VERSION"].nil? &&
Bundler.rubygems.supports_bundler_trampolining? &&
+ ruby_can_restart_with_same_arguments? &&
SharedHelpers.in_bundle? &&
lockfile_version
end
@@ -113,7 +129,7 @@ module Bundler
end
def local_specs
- @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true, "allow_cached" => true).specs.select {|spec| spec.name == "bundler" }
+ @local_specs ||= Bundler::Source::Rubygems.new("allow_local" => true).specs.select {|spec| spec.name == "bundler" }
end
def remote_specs
@@ -151,6 +167,10 @@ module Bundler
!version.to_s.end_with?(".dev")
end
+ def ruby_can_restart_with_same_arguments?
+ $PROGRAM_NAME != "-e"
+ end
+
def updating?
"update".start_with?(ARGV.first || " ") && ARGV[1..-1].any? {|a| a.start_with?("--bundler") }
end
diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb
index abbaec9783..878747a53b 100644
--- a/lib/bundler/settings.rb
+++ b/lib/bundler/settings.rb
@@ -103,6 +103,7 @@ module Bundler
def initialize(root = nil)
@root = root
@local_config = load_config(local_config_file)
+ @local_root = root || Pathname.new(".bundle").expand_path
@env_config = ENV.to_h
@env_config.select! {|key, _value| key.start_with?("BUNDLE_") }
@@ -142,7 +143,7 @@ module Bundler
end
def set_local(key, value)
- local_config_file || raise(GemfileNotFound, "Could not locate Gemfile")
+ local_config_file = @local_root.join("config")
set_key(key, value, @local_config, local_config_file)
end
@@ -491,6 +492,10 @@ module Bundler
valid_file = file.exist? && !file.size.zero?
return {} unless valid_file
serializer_class.load(file.read).inject({}) do |config, (k, v)|
+ k = k.dup
+ k << "/" if /https?:/i.match?(k) && !k.end_with?("/", "__#{FALLBACK_TIMEOUT_URI_OPTION.upcase}")
+ k.gsub!(".", "__")
+
unless k.start_with?("#")
if k.include?("-")
Bundler.ui.warn "Your #{file} config includes `#{k}`, which contains the dash character (`-`).\n" \
@@ -518,26 +523,25 @@ module Bundler
YAMLSerializer
end
- PER_URI_OPTIONS = %w[
- fallback_timeout
- ].freeze
+ FALLBACK_TIMEOUT_URI_OPTION = "fallback_timeout"
NORMALIZE_URI_OPTIONS_PATTERN =
/
\A
(\w+\.)? # optional prefix key
(https?.*?) # URI
- (\.#{Regexp.union(PER_URI_OPTIONS)})? # optional suffix key
+ (\.#{FALLBACK_TIMEOUT_URI_OPTION})? # optional suffix key
\z
/ix
def self.key_for(key)
- key = normalize_uri(key).to_s if key.is_a?(String) && key.start_with?("http", "mirror.http")
- key = key_to_s(key).gsub(".", "__")
+ key = key_to_s(key)
+ key = normalize_uri(key) if key.start_with?("http", "mirror.http")
+ key = key.gsub(".", "__")
key.gsub!("-", "___")
key.upcase!
- key.prepend("BUNDLE_")
+ key.gsub(/\A([ #]*)/, '\1BUNDLE_')
end
# TODO: duplicates Rubygems#normalize_uri
diff --git a/lib/bundler/setup.rb b/lib/bundler/setup.rb
index 6010d66742..5a0fd8e0e3 100644
--- a/lib/bundler/setup.rb
+++ b/lib/bundler/setup.rb
@@ -5,6 +5,9 @@ require_relative "shared_helpers"
if Bundler::SharedHelpers.in_bundle?
require_relative "../bundler"
+ # autoswitch to locked Bundler version if available
+ Bundler.auto_switch
+
# try to auto_install first before we get to the `Bundler.ui.silence`, so user knows what is happening
Bundler.auto_install
diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb
index 28f0cdff19..e55632b89f 100644
--- a/lib/bundler/shared_helpers.rb
+++ b/lib/bundler/shared_helpers.rb
@@ -4,14 +4,14 @@ require_relative "version"
require_relative "rubygems_integration"
require_relative "current_ruby"
+autoload :Pathname, "pathname"
+
module Bundler
autoload :WINDOWS, File.expand_path("constants", __dir__)
autoload :FREEBSD, File.expand_path("constants", __dir__)
autoload :NULL, File.expand_path("constants", __dir__)
module SharedHelpers
- autoload :Pathname, "pathname"
-
def root
gemfile = find_gemfile
raise GemfileNotFound, "Could not locate Gemfile" unless gemfile
diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb
index 198e335bb6..c52ce04638 100644
--- a/lib/bundler/source/git.rb
+++ b/lib/bundler/source/git.rb
@@ -32,6 +32,20 @@ module Bundler
@local = false
end
+ def remote!
+ return if @allow_remote
+
+ @local_specs = nil
+ @allow_remote = true
+ end
+
+ def cached!
+ return if @allow_cached
+
+ @local_specs = nil
+ @allow_cached = true
+ end
+
def self.from_lock(options)
new(options.merge("uri" => options.delete("remote")))
end
@@ -56,13 +70,13 @@ module Bundler
end
def hash
- [self.class, uri, ref, branch, name, version, glob, submodules].hash
+ [self.class, uri, ref, branch, name, glob, submodules].hash
end
def eql?(other)
other.is_a?(Git) && uri == other.uri && ref == other.ref &&
branch == other.branch && name == other.name &&
- version == other.version && glob == other.glob &&
+ glob == other.glob &&
submodules == other.submodules
end
@@ -150,7 +164,8 @@ module Bundler
"does not exist. Run `bundle config unset local.#{override_for(original_path)}` to remove the local override"
end
- set_local!(path)
+ @local = true
+ set_paths!(path)
# Create a new git proxy without the cached revision
# so the Gemfile.lock always picks up the new revision.
@@ -173,13 +188,13 @@ module Bundler
end
def specs(*)
- set_local!(app_cache_path) if has_app_cache? && !local?
+ set_cache_path!(app_cache_path) if use_app_cache?
if requires_checkout? && !@copied
+ FileUtils.rm_rf(app_cache_path) if use_app_cache? && git_proxy.not_a_bare_repository?
+
fetch
- git_proxy.copy_to(install_path, submodules)
- serialize_gemspecs_in(install_path)
- @copied = true
+ checkout
end
local_specs
@@ -192,10 +207,7 @@ module Bundler
print_using_message "Using #{version_message(spec, options[:previous_spec])} from #{self}"
if (requires_checkout? && !@copied) || force
- Bundler.ui.debug " * Checking out revision: #{ref}"
- git_proxy.copy_to(install_path, submodules)
- serialize_gemspecs_in(install_path)
- @copied = true
+ checkout
end
generate_bin_options = { disable_extensions: !Bundler.rubygems.spec_missing_extensions?(spec), build_args: options[:build_args] }
@@ -207,12 +219,14 @@ module Bundler
def cache(spec, custom_path = nil)
app_cache_path = app_cache_path(custom_path)
return unless Bundler.feature_flag.cache_all?
- return if path == app_cache_path
+ return if install_path == app_cache_path
+ return if cache_path == app_cache_path
cached!
FileUtils.rm_rf(app_cache_path)
git_proxy.checkout if requires_checkout?
- git_proxy.copy_to(app_cache_path, @submodules)
- serialize_gemspecs_in(app_cache_path)
+ FileUtils.cp_r("#{cache_path}/.", app_cache_path)
+ FileUtils.touch(app_cache_path.join(".bundlecache"))
+ FileUtils.rm_rf(Dir.glob(app_cache_path.join("hooks/*.sample")))
end
def load_spec_files
@@ -256,6 +270,13 @@ module Bundler
private
+ def checkout
+ Bundler.ui.debug " * Checking out revision: #{ref}"
+ git_proxy.copy_to(install_path, submodules)
+ serialize_gemspecs_in(install_path)
+ @copied = true
+ end
+
def humanized_ref
if local?
path
@@ -278,22 +299,35 @@ module Bundler
# The gemspecs we cache should already be evaluated.
spec = Bundler.load_gemspec(spec_path)
next unless spec
- Bundler.rubygems.set_installed_by_version(spec)
+ spec.installed_by_version = Gem::VERSION
Bundler.rubygems.validate(spec)
File.open(spec_path, "wb") {|file| file.write(spec.to_ruby) }
end
end
- def set_local!(path)
- @local = true
- @local_specs = @git_proxy = nil
- @cache_path = @install_path = path
+ def set_paths!(path)
+ set_cache_path!(path)
+ set_install_path!(path)
+ end
+
+ def set_cache_path!(path)
+ @git_proxy = nil
+ @cache_path = path
+ end
+
+ def set_install_path!(path)
+ @local_specs = nil
+ @install_path = path
end
def has_app_cache?
cached_revision && super
end
+ def use_app_cache?
+ has_app_cache? && !local?
+ end
+
def requires_checkout?
allow_git_ops? && !local? && !cached_revision_checked_out?
end
@@ -359,9 +393,12 @@ module Bundler
def validate_spec(_spec); end
def load_gemspec(file)
- stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent)
- stub.full_gem_path = Pathname.new(file).dirname.expand_path(root).to_s
- StubSpecification.from_stub(stub)
+ dirname = Pathname.new(file).dirname
+ SharedHelpers.chdir(dirname.to_s) do
+ stub = Gem::StubSpecification.gemspec_stub(file, install_path.parent, install_path.parent)
+ stub.full_gem_path = dirname.expand_path(root).to_s
+ StubSpecification.from_stub(stub)
+ end
end
def git_scope
diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb
index 645851286c..768d40392f 100644
--- a/lib/bundler/source/git/git_proxy.rb
+++ b/lib/bundler/source/git/git_proxy.rb
@@ -84,6 +84,10 @@ module Bundler
end
end
+ def not_a_bare_repository?
+ git_local("rev-parse", "--is-bare-repository", dir: path).strip == "false"
+ end
+
def contains?(commit)
allowed_with_path do
result, status = git_null("branch", "--contains", commit, dir: path)
@@ -182,6 +186,14 @@ module Bundler
if err.include?("Could not find remote branch")
raise MissingGitRevisionError.new(command_with_no_credentials, nil, explicit_ref, credential_filtered_uri)
else
+ idx = command.index("--depth")
+ if idx
+ command.delete_at(idx)
+ command.delete_at(idx)
+ command_with_no_credentials = check_allowed(command)
+
+ err += "Retrying without --depth argument."
+ end
raise GitCommandError.new(command_with_no_credentials, path, err)
end
end
@@ -324,8 +336,6 @@ module Bundler
config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
remote.userinfo ||= config_auth
remote.to_s
- elsif File.exist?(uri)
- "file://#{uri}"
else
uri.to_s
end
diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb
index 978b0b2c9f..6a99f8e977 100644
--- a/lib/bundler/source/path.rb
+++ b/lib/bundler/source/path.rb
@@ -18,9 +18,6 @@ module Bundler
@options = options.dup
@glob = options["glob"] || DEFAULT_GLOB
- @allow_cached = false
- @allow_remote = false
-
@root_path = options["root_path"] || root
if options["path"]
@@ -41,16 +38,6 @@ module Bundler
@original_path = @path
end
- def remote!
- @local_specs = nil
- @allow_remote = true
- end
-
- def cached!
- @local_specs = nil
- @allow_cached = true
- end
-
def self.from_lock(options)
new(options.merge("path" => options.delete("remote")))
end
@@ -161,7 +148,7 @@ module Bundler
def load_gemspec(file)
return unless spec = Bundler.load_gemspec(file)
- Bundler.rubygems.set_installed_by_version(spec)
+ spec.installed_by_version = Gem::VERSION
spec
end
diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb
index 2e76becb84..3b6ef8bd58 100644
--- a/lib/bundler/source/rubygems.rb
+++ b/lib/bundler/source/rubygems.rb
@@ -17,11 +17,13 @@ module Bundler
@remotes = []
@dependency_names = []
@allow_remote = false
- @allow_cached = options["allow_cached"] || false
+ @allow_cached = false
@allow_local = options["allow_local"] || false
@checksum_store = Checksum::Store.new
Array(options["remotes"]).reverse_each {|r| add_remote(r) }
+
+ @lockfile_remotes = @remotes if options["from_lockfile"]
end
def caches
@@ -50,10 +52,11 @@ module Bundler
end
def cached!
+ return unless File.exist?(cache_path)
+
return if @allow_cached
@specs = nil
- @allow_local = true
@allow_cached = true
end
@@ -90,13 +93,13 @@ module Bundler
def self.from_lock(options)
options["remotes"] = Array(options.delete("remote")).reverse
- new(options)
+ new(options.merge("from_lockfile" => true))
end
def to_lock
out = String.new("GEM\n")
- remotes.reverse_each do |remote|
- out << " remote: #{remove_auth remote}\n"
+ lockfile_remotes.reverse_each do |remote|
+ out << " remote: #{remote}\n"
end
out << " specs:\n"
end
@@ -135,20 +138,17 @@ module Bundler
index = @allow_remote ? remote_specs.dup : Index.new
index.merge!(cached_specs) if @allow_cached
index.merge!(installed_specs) if @allow_local
+
+ # complete with default specs, only if not already available in the
+ # index through remote, cached, or installed specs
+ index.use(default_specs) if @allow_local
+
index
end
end
def install(spec, options = {})
- force = options[:force]
- ensure_builtin_gems_cached = options[:ensure_builtin_gems_cached]
-
- if ensure_builtin_gems_cached && spec.default_gem? && !cached_path(spec)
- cached_built_in_gem(spec) unless spec.remote
- force = true
- end
-
- if installed?(spec) && !force
+ if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
print_using_message "Using #{version_message(spec, options[:previous_spec])}"
return nil # no post-install message
end
@@ -206,6 +206,7 @@ module Bundler
spec.full_gem_path = installed_spec.full_gem_path
spec.loaded_from = installed_spec.loaded_from
+ spec.base_dir = installed_spec.base_dir
spec.post_install_message
end
@@ -221,12 +222,13 @@ module Bundler
raise InstallError, e.message
end
- def cached_built_in_gem(spec)
- cached_path = cached_path(spec)
- if cached_path.nil?
+ def cached_built_in_gem(spec, local: false)
+ cached_path = cached_gem(spec)
+ if cached_path.nil? && !local
remote_spec = remote_specs.search(spec).first
if remote_spec
cached_path = fetch_gem(remote_spec)
+ spec.remote = remote_spec.remote
else
Bundler.ui.warn "#{spec.full_name} is built in to Ruby, and can't be cached because your Gemfile doesn't have any sources that contain it."
end
@@ -323,14 +325,6 @@ module Bundler
end
def cached_gem(spec)
- if spec.default_gem?
- cached_built_in_gem(spec)
- else
- cached_path(spec)
- end
- end
-
- def cached_path(spec)
global_cache_path = download_cache_path(spec)
caches << global_cache_path if global_cache_path
@@ -361,7 +355,7 @@ module Bundler
def installed_specs
@installed_specs ||= Index.build do |idx|
- Bundler.rubygems.all_specs.reverse_each do |spec|
+ Bundler.rubygems.installed_specs.reverse_each do |spec|
spec.source = self
if Bundler.rubygems.spec_missing_extensions?(spec, false)
Bundler.ui.debug "Source #{self} is ignoring #{spec} because it is missing extensions"
@@ -372,6 +366,15 @@ module Bundler
end
end
+ def default_specs
+ @default_specs ||= Index.build do |idx|
+ Bundler.rubygems.default_specs.each do |spec|
+ spec.source = self
+ idx << spec
+ end
+ end
+ end
+
def cached_specs
@cached_specs ||= begin
idx = Index.new
@@ -456,6 +459,10 @@ module Bundler
private
+ def lockfile_remotes
+ @lockfile_remotes || credless_remotes
+ end
+
# Checks if the requested spec exists in the global cache. If it does,
# we copy it to the download path, and if it does not, we download it.
#
diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb
index bbaac33a95..5f9dd68f17 100644
--- a/lib/bundler/source_list.rb
+++ b/lib/bundler/source_list.rb
@@ -9,7 +9,7 @@ module Bundler
:metadata_source
def global_rubygems_source
- @global_rubygems_source ||= rubygems_aggregate_class.new("allow_local" => true, "allow_cached" => true)
+ @global_rubygems_source ||= rubygems_aggregate_class.new("allow_local" => true)
end
def initialize
@@ -22,6 +22,7 @@ module Bundler
@metadata_source = Source::Metadata.new
@merged_gem_lockfile_sections = false
+ @local_mode = true
end
def merged_gem_lockfile_sections?
@@ -73,6 +74,10 @@ module Bundler
global_rubygems_source
end
+ def local_mode?
+ @local_mode
+ end
+
def default_source
global_path_source || global_rubygems_source
end
@@ -140,11 +145,17 @@ module Bundler
all_sources.each(&:local_only!)
end
+ def local!
+ all_sources.each(&:local!)
+ end
+
def cached!
all_sources.each(&:cached!)
end
def remote!
+ @local_mode = false
+
all_sources.each(&:remote!)
end
@@ -178,7 +189,7 @@ module Bundler
replacement_source = replace_rubygems_source(replacement_sources, global_rubygems_source)
return global_rubygems_source unless replacement_source
- replacement_source.cached!
+ replacement_source.local!
replacement_source
end
diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb
index 2933d28450..96c36c2dec 100644
--- a/lib/bundler/spec_set.rb
+++ b/lib/bundler/spec_set.rb
@@ -71,12 +71,6 @@ module Bundler
platforms
end
- def complete_platforms!(platforms)
- platforms.each do |platform|
- complete_platform(platform)
- end
- end
-
def validate_deps(s)
s.runtime_dependencies.each do |dep|
next if dep.name == "bundler"
@@ -100,7 +94,7 @@ module Bundler
end
def delete(specs)
- specs.each {|spec| @specs.delete(spec) }
+ Array(specs).each {|spec| @specs.delete(spec) }
reset!
end
@@ -158,6 +152,12 @@ module Bundler
@specs.detect {|spec| spec.name == name && spec.match_platform(platform) }
end
+ def specs_compatible_with(other)
+ select do |spec|
+ other.valid?(spec)
+ end
+ end
+
def delete_by_name(name)
@specs.reject! {|spec| spec.name == name }
@@ -195,6 +195,10 @@ module Bundler
lookup.keys
end
+ def valid?(s)
+ s.matches_current_metadata? && valid_dependencies?(s)
+ end
+
private
def reset!
@@ -209,7 +213,7 @@ module Bundler
spec = specs.first
matching_specs = spec.source.specs.search([spec.name, spec.version])
platform_spec = GemHelpers.select_best_platform_match(matching_specs, platform).find do |s|
- s.matches_current_metadata? && valid_dependencies?(s)
+ valid?(s)
end
if platform_spec
@@ -273,13 +277,11 @@ module Bundler
specs_for_name = lookup[dep.name]
return [] unless specs_for_name
- matching_specs = if dep.force_ruby_platform
- GemHelpers.force_ruby_platform(specs_for_name)
+ if platform
+ GemHelpers.select_best_platform_match(specs_for_name, platform, force_ruby: dep.force_ruby_platform)
else
- GemHelpers.select_best_platform_match(specs_for_name, platform || Bundler.local_platform)
+ GemHelpers.select_best_local_platform_match(specs_for_name, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform)
end
- matching_specs.map!(&:materialize_for_installation).compact! if platform.nil?
- matching_specs
end
def tsort_each_child(s)
diff --git a/lib/bundler/stub_specification.rb b/lib/bundler/stub_specification.rb
index da830cf8d4..1cbb506ef9 100644
--- a/lib/bundler/stub_specification.rb
+++ b/lib/bundler/stub_specification.rb
@@ -77,6 +77,14 @@ module Bundler
stub.full_require_paths
end
+ def require_paths
+ stub.require_paths
+ end
+
+ def base_dir=(path)
+ stub.base_dir = path
+ end
+
def load_paths
full_require_paths
end
diff --git a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt
index 175b821a62..67fe8cee79 100644
--- a/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt
+++ b/lib/bundler/templates/newgem/CODE_OF_CONDUCT.md.tt
@@ -2,83 +2,131 @@
## Our Pledge
-We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
-We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
## Our Standards
-Examples of behavior that contributes to a positive environment for our community include:
+Examples of behavior that contributes to a positive environment for our
+community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall community
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
Examples of unacceptable behavior include:
-* The use of sexualized language or imagery, and sexual attention or
- advances of any kind
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing others' private information, such as a physical or email
- address, without their explicit permission
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
-Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
-Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
## Scope
-This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
## Enforcement
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at <%= config[:email] %>. All complaints will be reviewed and investigated promptly and fairly.
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
-All community leaders are obligated to respect the privacy and security of the reporter of any incident.
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
## Enforcement Guidelines
-Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
-**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
-**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
### 2. Warning
-**Community Impact**: A violation through a single incident or series of actions.
+**Community Impact**: A violation through a single incident or series of
+actions.
-**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
### 3. Temporary Ban
-**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
-**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
-**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
-**Consequence**: A permanent ban from any sort of public interaction within the community.
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
-available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
-Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/lib/bundler/templates/newgem/README.md.tt b/lib/bundler/templates/newgem/README.md.tt
index 5bf36378e8..f9c97d5c7e 100644
--- a/lib/bundler/templates/newgem/README.md.tt
+++ b/lib/bundler/templates/newgem/README.md.tt
@@ -10,11 +10,15 @@ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_O
Install the gem and add to the application's Gemfile by executing:
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```bash
+bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```
If bundler is not being used to manage dependencies, install the gem by executing:
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```bash
+gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
+```
## Usage
diff --git a/lib/bundler/ui/shell.rb b/lib/bundler/ui/shell.rb
index 4555612dbb..6df1512a5b 100644
--- a/lib/bundler/ui/shell.rb
+++ b/lib/bundler/ui/shell.rb
@@ -6,14 +6,17 @@ module Bundler
module UI
class Shell
LEVELS = %w[silent error warn confirm info debug].freeze
+ OUTPUT_STREAMS = [:stdout, :stderr].freeze
attr_writer :shell
+ attr_reader :output_stream
def initialize(options = {})
Thor::Base.shell = options["no-color"] ? Thor::Shell::Basic : nil
@shell = Thor::Base.shell.new
@level = ENV["DEBUG"] ? "debug" : "info"
@warning_history = []
+ @output_stream = :stdout
end
def add_color(string, *color)
@@ -84,7 +87,7 @@ module Bundler
@shell.yes?(msg)
end
- def no?
+ def no?(msg)
@shell.no?(msg)
end
@@ -101,6 +104,11 @@ module Bundler
index <= LEVELS.index(@level)
end
+ def output_stream=(symbol)
+ raise ArgumentError unless OUTPUT_STREAMS.include?(symbol)
+ @output_stream = symbol
+ end
+
def trace(e, newline = nil, force = false)
return unless debug? || force
msg = "#{e.class}: #{e.message}\n#{e.backtrace.join("\n ")}"
@@ -111,6 +119,10 @@ module Bundler
with_level("silent", &blk)
end
+ def progress(&blk)
+ with_output_stream(:stderr, &blk)
+ end
+
def unprinted_warnings
[]
end
@@ -119,6 +131,8 @@ module Bundler
# valimism
def tell_me(msg, color = nil, newline = nil)
+ return tell_err(msg, color, newline) if output_stream == :stderr
+
msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap]
if newline.nil?
@shell.say(msg, color)
@@ -130,7 +144,7 @@ module Bundler
def tell_err(message, color = nil, newline = nil)
return if @shell.send(:stderr).closed?
- newline ||= !message.to_s.match?(/( |\t)\Z/)
+ newline = !message.to_s.match?(/( |\t)\Z/) if newline.nil?
message = word_wrap(message) if newline.is_a?(Hash) && newline[:wrap]
color = nil if color && !$stderr.tty?
@@ -160,6 +174,14 @@ module Bundler
ensure
@level = original
end
+
+ def with_output_stream(symbol)
+ original = output_stream
+ self.output_stream = symbol
+ yield
+ ensure
+ @output_stream = original
+ end
end
end
end
diff --git a/lib/bundler/ui/silent.rb b/lib/bundler/ui/silent.rb
index fa3292bdc9..83d31d4b55 100644
--- a/lib/bundler/ui/silent.rb
+++ b/lib/bundler/ui/silent.rb
@@ -53,6 +53,13 @@ module Bundler
false
end
+ def output_stream=(_symbol)
+ end
+
+ def output_stream
+ nil
+ end
+
def ask(message)
end
@@ -60,7 +67,7 @@ module Bundler
raise "Cannot ask yes? with a silent shell"
end
- def no?
+ def no?(msg)
raise "Cannot ask no? with a silent shell"
end
@@ -77,6 +84,10 @@ module Bundler
yield
end
+ def progress
+ yield
+ end
+
def unprinted_warnings
@warnings
end
diff --git a/lib/bundler/vendor/securerandom/.document b/lib/bundler/vendor/securerandom/.document
new file mode 100644
index 0000000000..0c43bbd6b3
--- /dev/null
+++ b/lib/bundler/vendor/securerandom/.document
@@ -0,0 +1 @@
+# Vendored files do not need to be documented
diff --git a/lib/bundler/vendor/securerandom/lib/random/formatter.rb b/lib/bundler/vendor/securerandom/lib/random/formatter.rb
new file mode 100644
index 0000000000..e429709789
--- /dev/null
+++ b/lib/bundler/vendor/securerandom/lib/random/formatter.rb
@@ -0,0 +1,373 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+# == \Random number formatter.
+#
+# Formats generated random numbers in many manners. When <tt>'random/formatter'</tt>
+# is required, several methods are added to empty core module <tt>Bundler::Random::Formatter</tt>,
+# making them available as Random's instance and module methods.
+#
+# Standard library Bundler::SecureRandom is also extended with the module, and the methods
+# described below are available as a module methods in it.
+#
+# === Examples
+#
+# Generate random hexadecimal strings:
+#
+# require 'bundler/vendor/securerandom/lib/random/formatter'
+#
+# prng = Random.new
+# prng.hex(10) #=> "52750b30ffbc7de3b362"
+# prng.hex(10) #=> "92b15d6c8dc4beb5f559"
+# prng.hex(13) #=> "39b290146bea6ce975c37cfc23"
+# # or just
+# Random.hex #=> "1aed0c631e41be7f77365415541052ee"
+#
+# Generate random base64 strings:
+#
+# prng.base64(10) #=> "EcmTPZwWRAozdA=="
+# prng.base64(10) #=> "KO1nIU+p9DKxGg=="
+# prng.base64(12) #=> "7kJSM/MzBJI+75j8"
+# Random.base64(4) #=> "bsQ3fQ=="
+#
+# Generate random binary strings:
+#
+# prng.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
+# prng.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
+# Random.random_bytes(6) #=> "\xA1\xE6Lr\xC43"
+#
+# Generate alphanumeric strings:
+#
+# prng.alphanumeric(10) #=> "S8baxMJnPl"
+# prng.alphanumeric(10) #=> "aOxAg8BAJe"
+# Random.alphanumeric #=> "TmP9OsJHJLtaZYhP"
+#
+# Generate UUIDs:
+#
+# prng.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+# prng.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+# Random.uuid #=> "f14e0271-de96-45cc-8911-8910292a42cd"
+#
+# All methods are available in the standard library Bundler::SecureRandom, too:
+#
+# Bundler::SecureRandom.hex #=> "05b45376a30c67238eb93b16499e50cf"
+
+module Bundler::Random::Formatter
+
+ # Generate a random binary string.
+ #
+ # The argument _n_ specifies the length of the result string.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in future.
+ #
+ # The result may contain any byte: "\x00" - "\xff".
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
+ # # or
+ # prng = Random.new
+ # prng.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
+ def random_bytes(n=nil)
+ n = n ? n.to_int : 16
+ gen_random(n)
+ end
+
+ # Generate a random hexadecimal string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number to be generated.
+ # The length of the resulting hexadecimal string is twice of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain 0-9 and a-f.
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
+ # # or
+ # prng = Random.new
+ # prng.hex #=> "91dc3bfb4de5b11d029d376634589b61"
+ def hex(n=nil)
+ random_bytes(n).unpack1("H*")
+ end
+
+ # Generate a random base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
+ # # or
+ # prng = Random.new
+ # prng.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
+ #
+ # See RFC 3548 for the definition of base64.
+ def base64(n=nil)
+ [random_bytes(n)].pack("m0")
+ end
+
+ # Generate a random URL-safe base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The boolean argument _padding_ specifies the padding.
+ # If it is false or nil, padding is not generated.
+ # Otherwise padding is generated.
+ # By default, padding is not generated because "=" may be used as a URL delimiter.
+ #
+ # The result may contain A-Z, a-z, 0-9, "-" and "_".
+ # "=" is also used if _padding_ is true.
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
+ # # or
+ # prng = Random.new
+ # prng.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
+ #
+ # prng.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
+ # prng.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
+ #
+ # See RFC 3548 for the definition of URL-safe base64.
+ def urlsafe_base64(n=nil, padding=false)
+ s = [random_bytes(n)].pack("m0")
+ s.tr!("+/", "-_")
+ s.delete!("=") unless padding
+ s
+ end
+
+ # Generate a random v4 UUID (Universally Unique IDentifier).
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+ # Random.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+ # # or
+ # prng = Random.new
+ # prng.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
+ #
+ # The version 4 UUID is purely random (except the version).
+ # It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
+ #
+ # The result contains 122 random bits (15.25 random bytes).
+ #
+ # See RFC4122[https://datatracker.ietf.org/doc/html/rfc4122] for details of UUID.
+ #
+ def uuid
+ ary = random_bytes(16).unpack("NnnnnN")
+ ary[2] = (ary[2] & 0x0fff) | 0x4000
+ ary[3] = (ary[3] & 0x3fff) | 0x8000
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
+ end
+
+ alias uuid_v4 uuid
+
+ # Generate a random v7 UUID (Universally Unique IDentifier).
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.uuid_v7 # => "0188d4c3-1311-7f96-85c7-242a7aa58f1e"
+ # Random.uuid_v7 # => "0188d4c3-16fe-744f-86af-38fa04c62bb5"
+ # Random.uuid_v7 # => "0188d4c3-1af8-764f-b049-c204ce0afa23"
+ # Random.uuid_v7 # => "0188d4c3-1e74-7085-b14f-ef6415dc6f31"
+ # # |<--sorted-->| |<----- random ---->|
+ #
+ # # or
+ # prng = Random.new
+ # prng.uuid_v7 # => "0188ca51-5e72-7950-a11d-def7ff977c98"
+ #
+ # The version 7 UUID starts with the least significant 48 bits of a 64 bit
+ # Unix timestamp (milliseconds since the epoch) and fills the remaining bits
+ # with random data, excluding the version and variant bits.
+ #
+ # This allows version 7 UUIDs to be sorted by creation time. Time ordered
+ # UUIDs can be used for better database index locality of newly inserted
+ # records, which may have a significant performance benefit compared to random
+ # data inserts.
+ #
+ # The result contains 74 random bits (9.25 random bytes).
+ #
+ # Note that this method cannot be made reproducable because its output
+ # includes not only random bits but also timestamp.
+ #
+ # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/]
+ # for details of UUIDv7.
+ #
+ # ==== Monotonicity
+ #
+ # UUIDv7 has millisecond precision by default, so multiple UUIDs created
+ # within the same millisecond are not issued in monotonically increasing
+ # order. To create UUIDs that are time-ordered with sub-millisecond
+ # precision, up to 12 bits of additional timestamp may added with
+ # +extra_timestamp_bits+. The extra timestamp precision comes at the expense
+ # of random bits. Setting <tt>extra_timestamp_bits: 12</tt> provides ~244ns
+ # of precision, but only 62 random bits (7.75 random bytes).
+ #
+ # prng = Random.new
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 12) }
+ # # =>
+ # ["0188d4c7-13da-74f9-8b53-22a786ffdd5a",
+ # "0188d4c7-13da-753b-83a5-7fb9b2afaeea",
+ # "0188d4c7-13da-754a-88ea-ac0baeedd8db",
+ # "0188d4c7-13da-7557-83e1-7cad9cda0d8d"]
+ # # |<--- sorted --->| |<-- random --->|
+ #
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 8) }
+ # # =>
+ # ["0188d4c7-3333-7a95-850a-de6edb858f7e",
+ # "0188d4c7-3333-7ae8-842e-bc3a8b7d0cf9", # <- out of order
+ # "0188d4c7-3333-7ae2-995a-9f135dc44ead", # <- out of order
+ # "0188d4c7-3333-7af9-87c3-8f612edac82e"]
+ # # |<--- sorted -->||<---- random --->|
+ #
+ # Any rollbacks of the system clock will break monotonicity. UUIDv7 is based
+ # on UTC, which excludes leap seconds and can rollback the clock. To avoid
+ # this, the system clock can synchronize with an NTP server configured to use
+ # a "leap smear" approach. NTP or PTP will also be needed to synchronize
+ # across distributed nodes.
+ #
+ # Counters and other mechanisms for stronger guarantees of monotonicity are
+ # not implemented. Applications with stricter requirements should follow
+ # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters]
+ # of the specification.
+ #
+ def uuid_v7(extra_timestamp_bits: 0)
+ case (extra_timestamp_bits = Integer(extra_timestamp_bits))
+ when 0 # min timestamp precision
+ ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
+ rand = random_bytes(10)
+ rand.setbyte(0, rand.getbyte(0) & 0x0f | 0x70) # version
+ rand.setbyte(2, rand.getbyte(2) & 0x3f | 0x80) # variant
+ "%08x-%04x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ rand.unpack("H4H4H12").join("-")
+ ]
+
+ when 12 # max timestamp precision
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ extra_bits = ns * 4096 / 1_000_000
+ rand = random_bytes(8)
+ rand.setbyte(0, rand.getbyte(0) & 0x3f | 0x80) # variant
+ "%08x-%04x-7%03x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ extra_bits,
+ rand.unpack("H4H12").join("-")
+ ]
+
+ when (0..12) # the generic version is slower than the special cases above
+ rand_a, rand_b1, rand_b2, rand_b3 = random_bytes(10).unpack("nnnN")
+ rand_mask_bits = 12 - extra_timestamp_bits
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ "%08x-%04x-%04x-%04x-%04x%08x" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ 0x7000 |
+ ((ns * (1 << extra_timestamp_bits) / 1_000_000) << rand_mask_bits) |
+ rand_a & ((1 << rand_mask_bits) - 1),
+ 0x8000 | (rand_b1 & 0x3fff),
+ rand_b2,
+ rand_b3
+ ]
+
+ else
+ raise ArgumentError, "extra_timestamp_bits must be in 0..12"
+ end
+ end
+
+ # Internal interface to Random; Generate random data _n_ bytes.
+ private def gen_random(n)
+ self.bytes(n)
+ end
+
+ # Generate a string that randomly draws from a
+ # source array of characters.
+ #
+ # The argument _source_ specifies the array of characters from which
+ # to generate the string.
+ # The argument _n_ specifies the length, in characters, of the string to be
+ # generated.
+ #
+ # The result may contain whatever characters are in the source array.
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # prng.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
+ # prng.choose([*'0'..'9'], 5) #=> "27309"
+ private def choose(source, n)
+ size = source.size
+ m = 1
+ limit = size
+ while limit * size <= 0x100000000
+ limit *= size
+ m += 1
+ end
+ result = ''.dup
+ while m <= n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ (m-is.length).times { is << 0 }
+ result << source.values_at(*is).join('')
+ n -= m
+ end
+ if 0 < n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ if is.length < n
+ (n-is.length).times { is << 0 }
+ else
+ is.pop while n < is.length
+ end
+ result.concat source.values_at(*is).join('')
+ end
+ result
+ end
+
+ # The default character list for #alphanumeric.
+ ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9']
+
+ # Generate a random alphanumeric string.
+ #
+ # The argument _n_ specifies the length, in characters, of the alphanumeric
+ # string to be generated.
+ # The argument _chars_ specifies the character list which the result is
+ # consist of.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z and 0-9, unless _chars_ is specified.
+ #
+ # require 'bundler/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.alphanumeric #=> "2BuBuLf3WfSKyQbR"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10) #=> "i6K93NdqiH"
+ #
+ # Random.alphanumeric(4, chars: [*"0".."9"]) #=> "2952"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10, chars: [*"!".."/"]) #=> ",.,++%/''."
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end
+end
diff --git a/lib/bundler/vendor/securerandom/lib/securerandom.rb b/lib/bundler/vendor/securerandom/lib/securerandom.rb
new file mode 100644
index 0000000000..e797054468
--- /dev/null
+++ b/lib/bundler/vendor/securerandom/lib/securerandom.rb
@@ -0,0 +1,96 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require_relative 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
+#
+# You can use this library in your application by requiring it:
+#
+# require 'bundler/vendor/securerandom/lib/securerandom'
+#
+# It supports the following secure random number generators:
+#
+# * openssl
+# * /dev/urandom
+# * Win32
+#
+# Bundler::SecureRandom is extended by the Bundler::Random::Formatter module which
+# defines the following methods:
+#
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
+#
+# These methods are usable as class methods of Bundler::SecureRandom such as
+# +Bundler::SecureRandom.hex+.
+#
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
+
+module Bundler::SecureRandom
+
+ # The version
+ VERSION = "0.3.1"
+
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
+ end
+
+ private
+
+ # :stopdoc:
+
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
+
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
+ end
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
+ end
+ ret
+ end
+
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
+ begin
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
+ end
+
+ # :startdoc:
+
+ # Generate random data bytes for Bundler::Random::Formatter
+ public :gen_random
+ end
+end
+
+Bundler::SecureRandom.extend(Bundler::Random::Formatter)
diff --git a/lib/bundler/vendored_net_http.rb b/lib/bundler/vendored_net_http.rb
index 0dcabaa7d7..8ff2ccd1fe 100644
--- a/lib/bundler/vendored_net_http.rb
+++ b/lib/bundler/vendored_net_http.rb
@@ -1,12 +1,23 @@
# frozen_string_literal: true
-begin
- require "rubygems/vendored_net_http"
-rescue LoadError
+# This defined? guard can be removed once RubyGems 3.4 support is dropped.
+#
+# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid
+# activating the Bundler gem too early. Without this guard, we get redefinition
+# warnings once Bundler is actually activated and
+# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in
+# RubyGems versions including `rubygems/vendored_net_http` since `require` takes
+# care of avoiding the double load.
+#
+unless defined?(Gem::Net)
begin
- require "rubygems/net/http"
+ require "rubygems/vendored_net_http"
rescue LoadError
- require "net/http"
- Gem::Net = Net
+ begin
+ require "rubygems/net/http"
+ rescue LoadError
+ require "net/http"
+ Gem::Net = Net
+ end
end
end
diff --git a/lib/bundler/vendored_securerandom.rb b/lib/bundler/vendored_securerandom.rb
new file mode 100644
index 0000000000..6c15f4a2b2
--- /dev/null
+++ b/lib/bundler/vendored_securerandom.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Use RubyGems vendored copy when available. Otherwise fallback to Bundler
+# vendored copy. The vendored copy in Bundler can be removed once support for
+# RubyGems 3.5.18 is dropped.
+
+begin
+ require "rubygems/vendored_securerandom"
+rescue LoadError
+ module Bundler::Random; end
+ require_relative "vendor/securerandom/lib/securerandom"
+ Gem::SecureRandom = Bundler::SecureRandom
+ Gem::Random = Bundler::Random
+end
diff --git a/lib/bundler/yaml_serializer.rb b/lib/bundler/yaml_serializer.rb
index 42e6aaf89d..ab1eb6dbcf 100644
--- a/lib/bundler/yaml_serializer.rb
+++ b/lib/bundler/yaml_serializer.rb
@@ -41,7 +41,7 @@ module Bundler
HASH_REGEX = /
^
([ ]*) # indentations
- (.+) # key
+ ([^#]+) # key excludes comment char '#'
(?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value)
[ ]?
(['"]?) # optional opening quote
@@ -60,7 +60,6 @@ module Bundler
indent, key, quote, val = match.captures
val = strip_comment(val)
- convert_to_backward_compatible_key!(key)
depth = indent.size / 2
if quote.empty? && val.empty?
new_hash = {}
@@ -92,14 +91,8 @@ module Bundler
end
end
- # for settings' keys
- def convert_to_backward_compatible_key!(key)
- key << "/" if /https?:/i.match?(key) && !%r{/\Z}.match?(key)
- key.gsub!(".", "__")
- end
-
class << self
- private :dump_hash, :convert_to_backward_compatible_key!
+ private :dump_hash
end
end
end
diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb
index b9c68b8eb8..1a29ee476c 100644
--- a/lib/error_highlight/base.rb
+++ b/lib/error_highlight/base.rb
@@ -60,14 +60,14 @@ module ErrorHighlight
rescue RuntimeError => error
# RubyVM::AbstractSyntaxTree.of raises an error with a message that
# includes "prism" when the ISEQ was compiled with the prism compiler.
- # In this case, we'll set the node to `nil`. In the future, we will
- # reparse with the prism parser and pass the parsed node to Spotter.
+ # In this case, we'll try to parse again with prism instead.
raise unless error.message.include?("prism")
+ prism_find(loc)
end
Spotter.new(node, **opts).spot
- when RubyVM::AbstractSyntaxTree::Node
+ when RubyVM::AbstractSyntaxTree::Node, Prism::Node
Spotter.new(obj, **opts).spot
else
@@ -81,6 +81,21 @@ module ErrorHighlight
return nil
end
+ # Accepts a Thread::Backtrace::Location object and returns a Prism::Node
+ # corresponding to the backtrace location in the source code.
+ def self.prism_find(location)
+ require "prism"
+ return nil if Prism::VERSION < "0.29.0"
+
+ absolute_path = location.absolute_path
+ return unless absolute_path
+
+ node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location)
+ Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id }
+ end
+
+ private_class_method :prism_find
+
class Spotter
class NonAscii < Exception; end
private_constant :NonAscii
@@ -113,31 +128,49 @@ module ErrorHighlight
def spot
return nil unless @node
- if OPT_GETCONSTANT_PATH && @node.type == :COLON2
+ if OPT_GETCONSTANT_PATH
# In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`)
# is compiled to one instruction (opt_getconstant_path).
# @node points to the node of the whole `Foo::Bar::Baz` even if `Foo`
# or `Foo::Bar` causes NameError.
# So we try to spot the sub-node that causes the NameError by using
# `NameError#name`.
- subnodes = []
- node = @node
- while node.type == :COLON2
- node2, const = node.children
- subnodes << node if const == @name
- node = node2
- end
- if node.type == :CONST || node.type == :COLON3
- if node.children.first == @name
+ case @node.type
+ when :COLON2
+ subnodes = []
+ node = @node
+ while node.type == :COLON2
+ node2, const = node.children
+ subnodes << node if const == @name
+ node = node2
+ end
+ if node.type == :CONST || node.type == :COLON3
+ if node.children.first == @name
+ subnodes << node
+ end
+
+ # If we found only one sub-node whose name is equal to @name, use it
+ return nil if subnodes.size != 1
+ @node = subnodes.first
+ else
+ # Do nothing; opt_getconstant_path is used only when the const base is
+ # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
+ end
+ when :constant_path_node
+ subnodes = []
+ node = @node
+
+ begin
+ subnodes << node if node.name == @name
+ end while (node = node.parent).is_a?(Prism::ConstantPathNode)
+
+ if node.is_a?(Prism::ConstantReadNode) && node.name == @name
subnodes << node
end
# If we found only one sub-node whose name is equal to @name, use it
return nil if subnodes.size != 1
@node = subnodes.first
- else
- # Do nothing; opt_getconstant_path is used only when the const base is
- # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`)
end
end
@@ -205,6 +238,48 @@ module ErrorHighlight
when :OP_CDECL
spot_op_cdecl
+
+ when :call_node
+ case @point_type
+ when :name
+ prism_spot_call_for_name
+ when :args
+ prism_spot_call_for_args
+ end
+
+ when :local_variable_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_local_variable_operator_write_for_name
+ when :args
+ prism_spot_local_variable_operator_write_for_args
+ end
+
+ when :call_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_call_operator_write_for_name
+ when :args
+ prism_spot_call_operator_write_for_args
+ end
+
+ when :index_operator_write_node
+ case @point_type
+ when :name
+ prism_spot_index_operator_write_for_name
+ when :args
+ prism_spot_index_operator_write_for_args
+ end
+
+ when :constant_read_node
+ prism_spot_constant_read
+
+ when :constant_path_node
+ prism_spot_constant_path
+
+ when :constant_path_operator_write_node
+ prism_spot_constant_path_operator_write
+
end
if @snippet && @beg_column && @end_column && @beg_column < @end_column
@@ -548,6 +623,204 @@ module ErrorHighlight
@beg_lineno = @end_lineno = lineno
@snippet = @fetch[lineno]
end
+
+ # Take a location from the prism parser and set the necessary instance
+ # variables.
+ def prism_location(location)
+ @beg_lineno = location.start_line
+ @beg_column = location.start_column
+ @end_lineno = location.end_line
+ @end_column = location.end_column
+ @snippet = @fetch[@beg_lineno, @end_lineno]
+ end
+
+ # Example:
+ # x.foo
+ # ^^^^
+ # x.foo(42)
+ # ^^^^
+ # x&.foo
+ # ^^^^^
+ # x[42]
+ # ^^^^
+ # x.foo = 1
+ # ^^^^^^
+ # x[42] = 1
+ # ^^^^^^
+ # x + 1
+ # ^
+ # +x
+ # ^
+ # foo(42)
+ # ^^^
+ # foo 42
+ # ^^^
+ # foo
+ # ^^^
+ def prism_spot_call_for_name
+ # Explicitly turn off foo.() syntax because error_highlight expects this
+ # to not work.
+ return nil if @node.name == :call && @node.message_loc.nil?
+
+ location = @node.message_loc || @node.call_operator_loc || @node.location
+ location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line
+
+ # If the method name ends with "=" but the message does not, then this is
+ # a method call using the "attribute assignment" syntax
+ # (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and
+ # add it to the location.
+ if (name = @node.name).end_with?("=") && !@node.message.end_with?("=")
+ location = location.adjoin("=")
+ end
+
+ prism_location(location)
+
+ if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/)
+ # If the method name is an operator, then error_highlight only
+ # highlights the first line.
+ fetch_line(location.start_line)
+ end
+ end
+
+ # Example:
+ # x.foo(42)
+ # ^^
+ # x[42]
+ # ^^
+ # x.foo = 1
+ # ^
+ # x[42] = 1
+ # ^^^^^^^
+ # x[] = 1
+ # ^^^^^
+ # x + 1
+ # ^
+ # foo(42)
+ # ^^
+ # foo 42
+ # ^^
+ def prism_spot_call_for_args
+ # Explicitly turn off foo.() syntax because error_highlight expects this
+ # to not work.
+ return nil if @node.name == :call && @node.message_loc.nil?
+
+ if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1
+ prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location))
+ else
+ prism_location(@node.arguments.location)
+ end
+ end
+
+ # Example:
+ # x += 1
+ # ^
+ def prism_spot_local_variable_operator_write_for_name
+ prism_location(@node.binary_operator_loc.chop)
+ end
+
+ # Example:
+ # x += 1
+ # ^
+ def prism_spot_local_variable_operator_write_for_args
+ prism_location(@node.value.location)
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^^ (for foo)
+ # x.foo += 42
+ # ^ (for +)
+ # x.foo += 42
+ # ^^^^^^^ (for foo=)
+ def prism_spot_call_operator_write_for_name
+ if !@name.start_with?(/[[:alpha:]_]/)
+ prism_location(@node.binary_operator_loc.chop)
+ else
+ location = @node.message_loc
+ if @node.call_operator_loc.start_line == location.start_line
+ location = @node.call_operator_loc.join(location)
+ end
+
+ location = location.adjoin("=") if @name.end_with?("=")
+ prism_location(location)
+ end
+ end
+
+ # Example:
+ # x.foo += 42
+ # ^^
+ def prism_spot_call_operator_write_for_args
+ prism_location(@node.value.location)
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^ (for [])
+ # x[1] += 42
+ # ^ (for +)
+ # x[1] += 42
+ # ^^^^^^ (for []=)
+ def prism_spot_index_operator_write_for_name
+ case @name
+ when :[]
+ prism_location(@node.opening_loc.join(@node.closing_loc))
+ when :[]=
+ prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("="))
+ else
+ # Explicitly turn off foo[] += 1 syntax when the operator is not on
+ # the same line because error_highlight expects this to not work.
+ return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line
+
+ prism_location(@node.binary_operator_loc.chop)
+ end
+ end
+
+ # Example:
+ # x[1] += 42
+ # ^^^^^^^^
+ def prism_spot_index_operator_write_for_args
+ opening_loc =
+ if @node.arguments.nil?
+ @node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1)
+ else
+ @node.arguments.location
+ end
+
+ prism_location(opening_loc.join(@node.value.location))
+ end
+
+ # Example:
+ # Foo
+ # ^^^
+ def prism_spot_constant_read
+ prism_location(@node.location)
+ end
+
+ # Example:
+ # Foo::Bar
+ # ^^^^^
+ def prism_spot_constant_path
+ if @node.parent && @node.parent.location.end_line == @node.location.end_line
+ fetch_line(@node.parent.location.end_line)
+ prism_location(@node.delimiter_loc.join(@node.name_loc))
+ else
+ fetch_line(@node.location.end_line)
+ location = @node.name_loc
+ location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line
+ prism_location(location)
+ end
+ end
+
+ # Example:
+ # Foo::Bar += 1
+ # ^^^^^^^^
+ def prism_spot_constant_path_operator_write
+ if @name == (target = @node.target).name
+ prism_location(target.delimiter_loc.join(target.name_loc))
+ else
+ prism_location(@node.binary_operator_loc.chop)
+ end
+ end
end
private_constant :Spotter
diff --git a/lib/fileutils.gemspec b/lib/fileutils.gemspec
index 76baea3039..2603d664da 100644
--- a/lib/fileutils.gemspec
+++ b/lib/fileutils.gemspec
@@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.description = "Several file utility methods for copying, moving, removing, etc."
s.require_path = %w{lib}
- s.files = ["LICENSE.txt", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"]
+ s.files = ["COPYING", "BSDL", "README.md", "Rakefile", "fileutils.gemspec", "lib/fileutils.rb"]
s.required_ruby_version = ">= 2.5.0"
s.authors = ["Minero Aoki"]
diff --git a/lib/find.gemspec b/lib/find.gemspec
index cb845e9409..aef24a5028 100644
--- a/lib/find.gemspec
+++ b/lib/find.gemspec
@@ -25,7 +25,5 @@ Gem::Specification.new do |spec|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
- spec.bindir = "exe"
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end
diff --git a/lib/irb.rb b/lib/irb.rb
index b3435c257e..213e231174 100644
--- a/lib/irb.rb
+++ b/lib/irb.rb
@@ -880,40 +880,42 @@ module IRB
# An exception raised by IRB.irb_abort
class Abort < Exception;end
- # The current IRB::Context of the session, see IRB.conf
- #
- # irb
- # irb(main):001:0> IRB.CurrentContext.irb_name = "foo"
- # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo"
- def IRB.CurrentContext # :nodoc:
- IRB.conf[:MAIN_CONTEXT]
- end
+ class << self
+ # The current IRB::Context of the session, see IRB.conf
+ #
+ # irb
+ # irb(main):001:0> IRB.CurrentContext.irb_name = "foo"
+ # foo(main):002:0> IRB.conf[:MAIN_CONTEXT].irb_name #=> "foo"
+ def CurrentContext # :nodoc:
+ conf[:MAIN_CONTEXT]
+ end
- # Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING`
- def IRB.start(ap_path = nil)
- STDOUT.sync = true
- $0 = File::basename(ap_path, ".rb") if ap_path
+ # Initializes IRB and creates a new Irb.irb object at the `TOPLEVEL_BINDING`
+ def start(ap_path = nil)
+ STDOUT.sync = true
+ $0 = File::basename(ap_path, ".rb") if ap_path
- IRB.setup(ap_path)
+ setup(ap_path)
- if @CONF[:SCRIPT]
- irb = Irb.new(nil, @CONF[:SCRIPT])
- else
- irb = Irb.new
+ if @CONF[:SCRIPT]
+ irb = Irb.new(nil, @CONF[:SCRIPT])
+ else
+ irb = Irb.new
+ end
+ irb.run(@CONF)
end
- irb.run(@CONF)
- end
- # Quits irb
- def IRB.irb_exit(*) # :nodoc:
- throw :IRB_EXIT, false
- end
+ # Quits irb
+ def irb_exit(*) # :nodoc:
+ throw :IRB_EXIT, false
+ end
- # Aborts then interrupts irb.
- #
- # Will raise an Abort exception, or the given `exception`.
- def IRB.irb_abort(irb, exception = Abort) # :nodoc:
- irb.context.thread.raise exception, "abort then interrupt!"
+ # Aborts then interrupts irb.
+ #
+ # Will raise an Abort exception, or the given `exception`.
+ def irb_abort(irb, exception = Abort) # :nodoc:
+ irb.context.thread.raise exception, "abort then interrupt!"
+ end
end
class Irb
@@ -1129,7 +1131,7 @@ module IRB
end
code.force_encoding(@context.io.encoding)
- if (command, arg = parse_command(code))
+ if (command, arg = @context.parse_command(code))
command_class = Command.load_command(command)
Statement::Command.new(code, command_class, arg)
else
@@ -1138,27 +1140,8 @@ module IRB
end
end
- def parse_command(code)
- command_name, arg = code.strip.split(/\s+/, 2)
- return unless code.lines.size == 1 && command_name
-
- arg ||= ''
- command = command_name.to_sym
- # Command aliases are always command. example: $, @
- if (alias_name = @context.command_aliases[command])
- return [alias_name, arg]
- end
-
- # Check visibility
- public_method = !!Kernel.instance_method(:public_method).bind_call(@context.main, command) rescue false
- private_method = !public_method && !!Kernel.instance_method(:method).bind_call(@context.main, command) rescue false
- if Command.execute_as_command?(command, public_method: public_method, private_method: private_method)
- [command, arg]
- end
- end
-
def command?(code)
- !!parse_command(code)
+ !!@context.parse_command(code)
end
def configure_io
diff --git a/lib/irb/command/base.rb b/lib/irb/command/base.rb
index 1d406630a2..af810ed343 100644
--- a/lib/irb/command/base.rb
+++ b/lib/irb/command/base.rb
@@ -10,8 +10,10 @@ module IRB
module Command
class CommandArgumentError < StandardError; end
- def self.extract_ruby_args(*args, **kwargs)
- throw :EXTRACT_RUBY_ARGS, [args, kwargs]
+ class << self
+ def extract_ruby_args(*args, **kwargs)
+ throw :EXTRACT_RUBY_ARGS, [args, kwargs]
+ end
end
class Base
@@ -31,6 +33,12 @@ module IRB
@help_message
end
+ def execute(irb_context, arg)
+ new(irb_context).execute(arg)
+ rescue CommandArgumentError => e
+ puts e.message
+ end
+
private
def highlight(text)
@@ -38,12 +46,6 @@ module IRB
end
end
- def self.execute(irb_context, arg)
- new(irb_context).execute(arg)
- rescue CommandArgumentError => e
- puts e.message
- end
-
def initialize(irb_context)
@irb_context = irb_context
end
diff --git a/lib/irb/command/cd.rb b/lib/irb/command/cd.rb
new file mode 100644
index 0000000000..b83c8689ae
--- /dev/null
+++ b/lib/irb/command/cd.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module IRB
+ module Command
+ class CD < Base
+ category "Workspace"
+ description "Move into the given object or leave the current context."
+
+ help_message(<<~HELP)
+ Usage: cd ([target]|..)
+
+ IRB uses a stack of workspaces to keep track of context(s), with `pushws` and `popws` commands to manipulate the stack.
+ The `cd` command is an attempt to simplify the operation and will be subject to change.
+
+ When given:
+ - an object, cd will use that object as the new context by pushing it onto the workspace stack.
+ - "..", cd will leave the current context by popping the top workspace off the stack.
+ - no arguments, cd will move to the top workspace on the stack by popping off all workspaces.
+
+ Examples:
+
+ cd Foo
+ cd Foo.new
+ cd @ivar
+ cd ..
+ cd
+ HELP
+
+ def execute(arg)
+ case arg
+ when ".."
+ irb_context.pop_workspace
+ when ""
+ # TODO: decide what workspace commands should be kept, and underlying APIs should look like,
+ # and perhaps add a new API to clear the workspace stack.
+ prev_workspace = irb_context.pop_workspace
+ while prev_workspace
+ prev_workspace = irb_context.pop_workspace
+ end
+ else
+ begin
+ obj = eval(arg, irb_context.workspace.binding)
+ irb_context.push_workspace(obj)
+ rescue StandardError => e
+ warn "Error: #{e}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/irb/command/debug.rb b/lib/irb/command/debug.rb
index 8a091a49ed..3ebb57fe54 100644
--- a/lib/irb/command/debug.rb
+++ b/lib/irb/command/debug.rb
@@ -58,13 +58,15 @@ module IRB
end
class DebugCommand < Debug
- def self.category
- "Debugging"
- end
+ class << self
+ def category
+ "Debugging"
+ end
- def self.description
- command_name = self.name.split("::").last.downcase
- "Start the debugger of debug.gem and run its `#{command_name}` command."
+ def description
+ command_name = self.name.split("::").last.downcase
+ "Start the debugger of debug.gem and run its `#{command_name}` command."
+ end
end
end
end
diff --git a/lib/irb/command/help.rb b/lib/irb/command/help.rb
index c2018f9b30..12b468fefc 100644
--- a/lib/irb/command/help.rb
+++ b/lib/irb/command/help.rb
@@ -39,7 +39,7 @@ module IRB
help_cmds = commands_grouped_by_categories.delete("Help")
no_category_cmds = commands_grouped_by_categories.delete("No category")
- aliases = irb_context.instance_variable_get(:@user_aliases).map do |alias_name, target|
+ aliases = irb_context.instance_variable_get(:@command_aliases).map do |alias_name, target|
{ display_name: alias_name, description: "Alias for `#{target}`" }
end
diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb
index a3d89373c3..7f102dcdf4 100644
--- a/lib/irb/completion.rb
+++ b/lib/irb/completion.rb
@@ -33,6 +33,8 @@ module IRB
yield
]
+ HELP_COMMAND_PREPOSING = /\Ahelp\s+/
+
def completion_candidates(preposing, target, postposing, bind:)
raise NotImplementedError
end
@@ -86,8 +88,8 @@ module IRB
)
end
- def command_completions(preposing, target)
- if preposing.empty? && !target.empty?
+ def command_candidates(target)
+ if !target.empty?
IRB::Command.command_names.select { _1.start_with?(target) }
else
[]
@@ -111,8 +113,18 @@ module IRB
end
def completion_candidates(preposing, target, _postposing, bind:)
- commands = command_completions(preposing, target)
+ # When completing the argument of `help` command, only commands should be candidates
+ return command_candidates(target) if preposing.match?(HELP_COMMAND_PREPOSING)
+
+ commands = if preposing.empty?
+ command_candidates(target)
+ # It doesn't make sense to propose commands with other preposing
+ else
+ []
+ end
+
result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)
+
return commands unless result
commands | result.completion_candidates.map { target + _1 }
@@ -187,12 +199,20 @@ module IRB
end
def completion_candidates(preposing, target, postposing, bind:)
- if preposing && postposing
- result = complete_require_path(target, preposing, postposing)
- return result if result
+ if result = complete_require_path(target, preposing, postposing)
+ return result
end
- commands = command_completions(preposing || '', target)
- commands | retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) }
+
+ commands = command_candidates(target)
+
+ # When completing the argument of `help` command, only commands should be candidates
+ return commands if preposing.match?(HELP_COMMAND_PREPOSING)
+
+ # It doesn't make sense to propose commands with other preposing
+ commands = [] unless preposing.empty?
+
+ completion_data = retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) }
+ commands | completion_data
end
def doc_namespace(_preposing, matched, _postposing, bind:)
@@ -470,7 +490,7 @@ module IRB
end
end
CompletionProc = ->(target, preposing = nil, postposing = nil) {
- regexp_completor.completion_candidates(preposing, target, postposing, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding)
+ regexp_completor.completion_candidates(preposing || '', target, postposing || '', bind: IRB.conf[:MAIN_CONTEXT].workspace.binding)
}
end
deprecate_constant :InputCompletor
diff --git a/lib/irb/context.rb b/lib/irb/context.rb
index aafce7aade..668a823f5c 100644
--- a/lib/irb/context.rb
+++ b/lib/irb/context.rb
@@ -13,6 +13,7 @@ module IRB
# A class that wraps the current state of the irb session, including the
# configuration of IRB.conf.
class Context
+ ASSIGN_OPERATORS_REGEXP = Regexp.union(%w[= += -= *= /= %= **= &= |= &&= ||= ^= <<= >>=])
# Creates a new IRB context.
#
# The optional +input_method+ argument:
@@ -148,8 +149,7 @@ module IRB
@newline_before_multiline_output = true
end
- @user_aliases = IRB.conf[:COMMAND_ALIASES].dup
- @command_aliases = @user_aliases.merge(KEYWORD_ALIASES)
+ @command_aliases = IRB.conf[:COMMAND_ALIASES].dup
end
private def term_interactive?
@@ -157,17 +157,6 @@ module IRB
STDIN.tty? && ENV['TERM'] != 'dumb'
end
- # because all input will eventually be evaluated as Ruby code,
- # command names that conflict with Ruby keywords need special workaround
- # we can remove them once we implemented a better command system for IRB
- KEYWORD_ALIASES = {
- :break => :irb_break,
- :catch => :irb_catch,
- :next => :irb_next,
- }.freeze
-
- private_constant :KEYWORD_ALIASES
-
def use_tracer=(val)
require_relative "ext/tracer" if val
IRB.conf[:USE_TRACER] = val
@@ -602,7 +591,6 @@ module IRB
set_last_value(result)
when Statement::Command
statement.command_class.execute(self, statement.arg)
- set_last_value(nil)
end
nil
@@ -636,6 +624,46 @@ module IRB
result
end
+ def parse_command(code)
+ command_name, arg = code.strip.split(/\s+/, 2)
+ return unless code.lines.size == 1 && command_name
+
+ arg ||= ''
+ command = command_name.to_sym
+ # Command aliases are always command. example: $, @
+ if (alias_name = command_aliases[command])
+ return [alias_name, arg]
+ end
+
+ # Assignment-like expression is not a command
+ return if arg.start_with?(ASSIGN_OPERATORS_REGEXP) && !arg.start_with?(/==|=~/)
+
+ # Local variable have precedence over command
+ return if local_variables.include?(command)
+
+ # Check visibility
+ public_method = !!Kernel.instance_method(:public_method).bind_call(main, command) rescue false
+ private_method = !public_method && !!Kernel.instance_method(:method).bind_call(main, command) rescue false
+ if Command.execute_as_command?(command, public_method: public_method, private_method: private_method)
+ [command, arg]
+ end
+ end
+
+ def colorize_input(input, complete:)
+ if IRB.conf[:USE_COLORIZE] && IRB::Color.colorable?
+ lvars = local_variables || []
+ if parse_command(input)
+ name, sep, arg = input.split(/(\s+)/, 2)
+ arg = IRB::Color.colorize_code(arg, complete: complete, local_variables: lvars)
+ "#{IRB::Color.colorize(name, [:BOLD])}\e[m#{sep}#{arg}"
+ else
+ IRB::Color.colorize_code(input, complete: complete, local_variables: lvars)
+ end
+ else
+ Reline::Unicode.escape_for_print(input)
+ end
+ end
+
def inspect_last_value # :nodoc:
@inspect_method.inspect_value(@last_value)
end
diff --git a/lib/irb/debug.rb b/lib/irb/debug.rb
index 1ec2335a8e..cd64b77ad7 100644
--- a/lib/irb/debug.rb
+++ b/lib/irb/debug.rb
@@ -57,22 +57,18 @@ module IRB
DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB)
end
- if !@output_modifier_defined && !DEBUGGER__::CONFIG[:no_hint]
- irb_output_modifier_proc = Reline.output_modifier_proc
-
- Reline.output_modifier_proc = proc do |output, complete:|
- unless output.strip.empty?
- cmd = output.split(/\s/, 2).first
+ if !DEBUGGER__::CONFIG[:no_hint] && irb.context.io.is_a?(RelineInputMethod)
+ Reline.output_modifier_proc = proc do |input, complete:|
+ unless input.strip.empty?
+ cmd = input.split(/\s/, 2).first
if !complete && DEBUGGER__.commands.key?(cmd)
- output = output.sub(/\n$/, " # debug command\n")
+ input = input.sub(/\n$/, " # debug command\n")
end
end
- irb_output_modifier_proc.call(output, complete: complete)
+ irb.context.colorize_input(input, complete: complete)
end
-
- @output_modifier_defined = true
end
true
diff --git a/lib/irb/default_commands.rb b/lib/irb/default_commands.rb
index 1bbc68efa7..768fbee9d7 100644
--- a/lib/irb/default_commands.rb
+++ b/lib/irb/default_commands.rb
@@ -2,32 +2,34 @@
require_relative "command"
require_relative "command/internal_helpers"
-require_relative "command/context"
-require_relative "command/exit"
-require_relative "command/force_exit"
-require_relative "command/chws"
-require_relative "command/pushws"
-require_relative "command/subirb"
-require_relative "command/load"
-require_relative "command/debug"
-require_relative "command/edit"
+require_relative "command/backtrace"
require_relative "command/break"
require_relative "command/catch"
-require_relative "command/next"
-require_relative "command/delete"
-require_relative "command/step"
+require_relative "command/cd"
+require_relative "command/chws"
+require_relative "command/context"
require_relative "command/continue"
+require_relative "command/debug"
+require_relative "command/delete"
+require_relative "command/disable_irb"
+require_relative "command/edit"
+require_relative "command/exit"
require_relative "command/finish"
-require_relative "command/backtrace"
-require_relative "command/info"
+require_relative "command/force_exit"
require_relative "command/help"
-require_relative "command/show_doc"
+require_relative "command/history"
+require_relative "command/info"
require_relative "command/irb_info"
+require_relative "command/load"
require_relative "command/ls"
require_relative "command/measure"
+require_relative "command/next"
+require_relative "command/pushws"
+require_relative "command/show_doc"
require_relative "command/show_source"
+require_relative "command/step"
+require_relative "command/subirb"
require_relative "command/whereami"
-require_relative "command/history"
module IRB
module Command
@@ -179,9 +181,15 @@ module IRB
[:edit, NO_OVERRIDE]
)
- _register_with_aliases(:irb_break, Command::Break)
- _register_with_aliases(:irb_catch, Command::Catch)
- _register_with_aliases(:irb_next, Command::Next)
+ _register_with_aliases(:irb_break, Command::Break,
+ [:break, OVERRIDE_ALL]
+ )
+ _register_with_aliases(:irb_catch, Command::Catch,
+ [:catch, OVERRIDE_PRIVATE_ONLY]
+ )
+ _register_with_aliases(:irb_next, Command::Next,
+ [:next, OVERRIDE_ALL]
+ )
_register_with_aliases(:irb_delete, Command::Delete,
[:delete, NO_OVERRIDE]
)
@@ -235,6 +243,12 @@ module IRB
[:history, NO_OVERRIDE],
[:hist, NO_OVERRIDE]
)
+
+ _register_with_aliases(:irb_disable_irb, Command::DisableIrb,
+ [:disable_irb, NO_OVERRIDE]
+ )
+
+ register(:cd, Command::CD)
end
ExtendCommand = Command
@@ -251,10 +265,12 @@ module IRB
# Deprecated. Doesn't have any effect.
@EXTEND_COMMANDS = []
- # Drepcated. Use Command.regiser instead.
- def self.def_extend_command(cmd_name, cmd_class, _, *aliases)
- Command._register_with_aliases(cmd_name, cmd_class, *aliases)
- Command.class_variable_set(:@@command_override_policies, nil)
+ class << self
+ # Drepcated. Use Command.regiser instead.
+ def def_extend_command(cmd_name, cmd_class, _, *aliases)
+ Command._register_with_aliases(cmd_name, cmd_class, *aliases)
+ Command.class_variable_set(:@@command_override_policies, nil)
+ end
end
end
end
diff --git a/lib/irb/easter-egg.rb b/lib/irb/easter-egg.rb
index adc0834d55..14dc93fc9c 100644
--- a/lib/irb/easter-egg.rb
+++ b/lib/irb/easter-egg.rb
@@ -100,19 +100,21 @@ module IRB
private def easter_egg_logo(type)
@easter_egg_logos ||= File.read(File.join(__dir__, 'ruby_logo.aa'), encoding: 'UTF-8:UTF-8')
- .split(/TYPE: ([A-Z]+)\n/)[1..]
+ .split(/TYPE: ([A-Z_]+)\n/)[1..]
.each_slice(2)
.to_h
@easter_egg_logos[type.to_s.upcase]
end
private def easter_egg(type = nil)
+ print "\e[?1049h"
type ||= [:logo, :dancing].sample
case type
when :logo
- require "rdoc"
- RDoc::RI::Driver.new.page do |io|
- io.write easter_egg_logo(:large)
+ Pager.page do |io|
+ logo_type = STDOUT.external_encoding == Encoding::UTF_8 ? :unicode_large : :ascii_large
+ io.write easter_egg_logo(logo_type)
+ STDIN.raw { STDIN.getc } if io == STDOUT
end
when :dancing
STDOUT.cooked do
@@ -137,10 +139,11 @@ module IRB
end
rescue Interrupt
ensure
- print "\e[0m\e[?1049l"
trap("SIGINT", prev_trap)
end
end
+ ensure
+ print "\e[0m\e[?1049l"
end
end
end
diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb
index 684527edc4..210d3da789 100644
--- a/lib/irb/input-method.rb
+++ b/lib/irb/input-method.rb
@@ -171,11 +171,13 @@ module IRB
end
class ReadlineInputMethod < StdioInputMethod
- def self.initialize_readline
- require "readline"
- rescue LoadError
- else
- include ::Readline
+ class << self
+ def initialize_readline
+ require "readline"
+ rescue LoadError
+ else
+ include ::Readline
+ end
end
include HistorySavingAbility
@@ -263,18 +265,9 @@ module IRB
@completion_params = [preposing, target, postposing, bind]
@completor.completion_candidates(preposing, target, postposing, bind: bind)
}
- Reline.output_modifier_proc =
- if IRB.conf[:USE_COLORIZE]
- proc do |output, complete: |
- next unless IRB::Color.colorable?
- lvars = IRB.CurrentContext&.local_variables || []
- IRB::Color.colorize_code(output, complete: complete, local_variables: lvars)
- end
- else
- proc do |output|
- Reline::Unicode.escape_for_print(output)
- end
- end
+ Reline.output_modifier_proc = proc do |input, complete:|
+ IRB.CurrentContext.colorize_input(input, complete: complete)
+ end
Reline.dig_perfect_match_proc = ->(matched) { display_document(matched) }
Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE]
@@ -328,10 +321,11 @@ module IRB
->() {
dialog.trap_key = nil
alt_d = [
- [Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d.
[27, 100], # Normal Alt+d when convert-meta isn't used.
- [195, 164], # The "ä" that appears when Alt+d is pressed on xterm.
- [226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2.
+ # When option/alt is not configured as a meta key in terminal emulator,
+ # option/alt + d will send a unicode character depend on OS keyboard setting.
+ [195, 164], # "ä" in somewhere (FIXME: environment information is unknown).
+ [226, 136, 130] # "∂" Alt+d on Mac keyboard.
]
if just_cursor_moving and completion_journey_data.nil?
diff --git a/lib/irb/inspector.rb b/lib/irb/inspector.rb
index 667087ccba..8046744f88 100644
--- a/lib/irb/inspector.rb
+++ b/lib/irb/inspector.rb
@@ -6,7 +6,6 @@
module IRB # :nodoc:
-
# Convenience method to create a new Inspector, using the given +inspect+
# proc, and optional +init+ proc and passes them to Inspector.new
#
@@ -43,38 +42,40 @@ module IRB # :nodoc:
# +:marshal+:: Using Marshal.dump
INSPECTORS = {}
- # Determines the inspector to use where +inspector+ is one of the keys passed
- # during inspector definition.
- def self.keys_with_inspector(inspector)
- INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k}
- end
-
- # Example
- #
- # Inspector.def_inspector(key, init_p=nil){|v| v.inspect}
- # Inspector.def_inspector([key1,..], init_p=nil){|v| v.inspect}
- # Inspector.def_inspector(key, inspector)
- # Inspector.def_inspector([key1,...], inspector)
- def self.def_inspector(key, arg=nil, &block)
- if block_given?
- inspector = IRB::Inspector(block, arg)
- else
- inspector = arg
+ class << self
+ # Determines the inspector to use where +inspector+ is one of the keys passed
+ # during inspector definition.
+ def keys_with_inspector(inspector)
+ INSPECTORS.select{|k, v| v == inspector}.collect{|k, v| k}
end
- case key
- when Array
- for k in key
- def_inspector(k, inspector)
+ # Example
+ #
+ # Inspector.def_inspector(key, init_p=nil){|v| v.inspect}
+ # Inspector.def_inspector([key1,..], init_p=nil){|v| v.inspect}
+ # Inspector.def_inspector(key, inspector)
+ # Inspector.def_inspector([key1,...], inspector)
+ def def_inspector(key, arg=nil, &block)
+ if block_given?
+ inspector = IRB::Inspector(block, arg)
+ else
+ inspector = arg
+ end
+
+ case key
+ when Array
+ for k in key
+ def_inspector(k, inspector)
+ end
+ when Symbol
+ INSPECTORS[key] = inspector
+ INSPECTORS[key.to_s] = inspector
+ when String
+ INSPECTORS[key] = inspector
+ INSPECTORS[key.intern] = inspector
+ else
+ INSPECTORS[key] = inspector
end
- when Symbol
- INSPECTORS[key] = inspector
- INSPECTORS[key.to_s] = inspector
- when String
- INSPECTORS[key] = inspector
- INSPECTORS[key.intern] = inspector
- else
- INSPECTORS[key] = inspector
end
end
diff --git a/lib/irb/nesting_parser.rb b/lib/irb/nesting_parser.rb
index 5aa940cc28..fc71d64aee 100644
--- a/lib/irb/nesting_parser.rb
+++ b/lib/irb/nesting_parser.rb
@@ -3,235 +3,237 @@ module IRB
module NestingParser
IGNORE_TOKENS = %i[on_sp on_ignored_nl on_comment on_embdoc_beg on_embdoc on_embdoc_end]
- # Scan each token and call the given block with array of token and other information for parsing
- def self.scan_opens(tokens)
- opens = []
- pending_heredocs = []
- first_token_on_line = true
- tokens.each do |t|
- skip = false
- last_tok, state, args = opens.last
- case state
- when :in_alias_undef
- skip = t.event == :on_kw
- when :in_unquoted_symbol
- unless IGNORE_TOKENS.include?(t.event)
- opens.pop
- skip = true
- end
- when :in_lambda_head
- opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do')
- when :in_method_head
- unless IGNORE_TOKENS.include?(t.event)
- next_args = []
- body = nil
- if args.include?(:receiver)
- case t.event
- when :on_lparen, :on_ivar, :on_gvar, :on_cvar
- # def (receiver). | def @ivar. | def $gvar. | def @@cvar.
- next_args << :dot
- when :on_kw
- case t.tok
- when 'self', 'true', 'false', 'nil'
- # def self(arg) | def self.
- next_args.push(:arg, :dot)
- else
- # def if(arg)
+ class << self
+ # Scan each token and call the given block with array of token and other information for parsing
+ def scan_opens(tokens)
+ opens = []
+ pending_heredocs = []
+ first_token_on_line = true
+ tokens.each do |t|
+ skip = false
+ last_tok, state, args = opens.last
+ case state
+ when :in_alias_undef
+ skip = t.event == :on_kw
+ when :in_unquoted_symbol
+ unless IGNORE_TOKENS.include?(t.event)
+ opens.pop
+ skip = true
+ end
+ when :in_lambda_head
+ opens.pop if t.event == :on_tlambeg || (t.event == :on_kw && t.tok == 'do')
+ when :in_method_head
+ unless IGNORE_TOKENS.include?(t.event)
+ next_args = []
+ body = nil
+ if args.include?(:receiver)
+ case t.event
+ when :on_lparen, :on_ivar, :on_gvar, :on_cvar
+ # def (receiver). | def @ivar. | def $gvar. | def @@cvar.
+ next_args << :dot
+ when :on_kw
+ case t.tok
+ when 'self', 'true', 'false', 'nil'
+ # def self(arg) | def self.
+ next_args.push(:arg, :dot)
+ else
+ # def if(arg)
+ skip = true
+ next_args << :arg
+ end
+ when :on_op, :on_backtick
+ # def +(arg)
skip = true
next_args << :arg
+ when :on_ident, :on_const
+ # def a(arg) | def a.
+ next_args.push(:arg, :dot)
end
- when :on_op, :on_backtick
- # def +(arg)
- skip = true
- next_args << :arg
- when :on_ident, :on_const
- # def a(arg) | def a.
- next_args.push(:arg, :dot)
end
- end
- if args.include?(:dot)
- # def receiver.name
- next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::')
- end
- if args.include?(:name)
- if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event)
- # def name(arg) | def receiver.name(arg)
- next_args << :arg
- skip = true
+ if args.include?(:dot)
+ # def receiver.name
+ next_args << :name if t.event == :on_period || (t.event == :on_op && t.tok == '::')
end
- end
- if args.include?(:arg)
- case t.event
- when :on_nl, :on_semicolon
- # def receiver.f;
- body = :normal
- when :on_lparen
- # def receiver.f()
- next_args << :eq
- else
+ if args.include?(:name)
+ if %i[on_ident on_const on_op on_kw on_backtick].include?(t.event)
+ # def name(arg) | def receiver.name(arg)
+ next_args << :arg
+ skip = true
+ end
+ end
+ if args.include?(:arg)
+ case t.event
+ when :on_nl, :on_semicolon
+ # def receiver.f;
+ body = :normal
+ when :on_lparen
+ # def receiver.f()
+ next_args << :eq
+ else
+ if t.event == :on_op && t.tok == '='
+ # def receiver.f =
+ body = :oneliner
+ else
+ # def receiver.f arg
+ next_args << :arg_without_paren
+ end
+ end
+ end
+ if args.include?(:eq)
if t.event == :on_op && t.tok == '='
- # def receiver.f =
body = :oneliner
else
- # def receiver.f arg
- next_args << :arg_without_paren
+ body = :normal
end
end
- end
- if args.include?(:eq)
- if t.event == :on_op && t.tok == '='
- body = :oneliner
- else
- body = :normal
+ if args.include?(:arg_without_paren)
+ if %i[on_semicolon on_nl].include?(t.event)
+ # def f a;
+ body = :normal
+ else
+ # def f a, b
+ next_args << :arg_without_paren
+ end
end
- end
- if args.include?(:arg_without_paren)
- if %i[on_semicolon on_nl].include?(t.event)
- # def f a;
- body = :normal
+ if body == :oneliner
+ opens.pop
+ elsif body
+ opens[-1] = [last_tok, nil]
else
- # def f a, b
- next_args << :arg_without_paren
+ opens[-1] = [last_tok, :in_method_head, next_args]
end
end
- if body == :oneliner
- opens.pop
- elsif body
+ when :in_for_while_until_condition
+ if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do')
+ skip = true if t.event == :on_kw && t.tok == 'do'
opens[-1] = [last_tok, nil]
- else
- opens[-1] = [last_tok, :in_method_head, next_args]
end
end
- when :in_for_while_until_condition
- if t.event == :on_semicolon || t.event == :on_nl || (t.event == :on_kw && t.tok == 'do')
- skip = true if t.event == :on_kw && t.tok == 'do'
- opens[-1] = [last_tok, nil]
- end
- end
- unless skip
- case t.event
- when :on_kw
- case t.tok
- when 'begin', 'class', 'module', 'do', 'case'
- opens << [t, nil]
- when 'end'
- opens.pop
- when 'def'
- opens << [t, :in_method_head, [:receiver, :name]]
- when 'if', 'unless'
- unless t.state.allbits?(Ripper::EXPR_LABEL)
+ unless skip
+ case t.event
+ when :on_kw
+ case t.tok
+ when 'begin', 'class', 'module', 'do', 'case'
opens << [t, nil]
- end
- when 'while', 'until'
- unless t.state.allbits?(Ripper::EXPR_LABEL)
- opens << [t, :in_for_while_until_condition]
- end
- when 'ensure', 'rescue'
- unless t.state.allbits?(Ripper::EXPR_LABEL)
+ when 'end'
+ opens.pop
+ when 'def'
+ opens << [t, :in_method_head, [:receiver, :name]]
+ when 'if', 'unless'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens << [t, nil]
+ end
+ when 'while', 'until'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens << [t, :in_for_while_until_condition]
+ end
+ when 'ensure', 'rescue'
+ unless t.state.allbits?(Ripper::EXPR_LABEL)
+ opens.pop
+ opens << [t, nil]
+ end
+ when 'alias'
+ opens << [t, :in_alias_undef, 2]
+ when 'undef'
+ opens << [t, :in_alias_undef, 1]
+ when 'elsif', 'else', 'when'
opens.pop
opens << [t, nil]
+ when 'for'
+ opens << [t, :in_for_while_until_condition]
+ when 'in'
+ if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line
+ opens.pop
+ opens << [t, nil]
+ end
end
- when 'alias'
- opens << [t, :in_alias_undef, 2]
- when 'undef'
- opens << [t, :in_alias_undef, 1]
- when 'elsif', 'else', 'when'
+ when :on_tlambda
+ opens << [t, :in_lambda_head]
+ when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg
+ opens << [t, nil]
+ when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end
+ opens.pop
+ when :on_heredoc_beg
+ pending_heredocs << t
+ when :on_heredoc_end
opens.pop
+ when :on_backtick
+ opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG)
+ when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg
opens << [t, nil]
- when 'for'
- opens << [t, :in_for_while_until_condition]
- when 'in'
- if last_tok&.event == :on_kw && %w[case in].include?(last_tok.tok) && first_token_on_line
- opens.pop
+ when :on_tstring_end, :on_regexp_end, :on_label_end
+ opens.pop
+ when :on_symbeg
+ if t.tok == ':'
+ opens << [t, :in_unquoted_symbol]
+ else
opens << [t, nil]
end
end
- when :on_tlambda
- opens << [t, :in_lambda_head]
- when :on_lparen, :on_lbracket, :on_lbrace, :on_tlambeg, :on_embexpr_beg, :on_embdoc_beg
- opens << [t, nil]
- when :on_rparen, :on_rbracket, :on_rbrace, :on_embexpr_end, :on_embdoc_end
- opens.pop
- when :on_heredoc_beg
- pending_heredocs << t
- when :on_heredoc_end
- opens.pop
- when :on_backtick
- opens << [t, nil] if t.state.allbits?(Ripper::EXPR_BEG)
- when :on_tstring_beg, :on_words_beg, :on_qwords_beg, :on_symbols_beg, :on_qsymbols_beg, :on_regexp_beg
- opens << [t, nil]
- when :on_tstring_end, :on_regexp_end, :on_label_end
- opens.pop
- when :on_symbeg
- if t.tok == ':'
- opens << [t, :in_unquoted_symbol]
- else
- opens << [t, nil]
- end
end
+ if t.event == :on_nl || t.event == :on_semicolon
+ first_token_on_line = true
+ elsif t.event != :on_sp
+ first_token_on_line = false
+ end
+ if pending_heredocs.any? && t.tok.include?("\n")
+ pending_heredocs.reverse_each { |t| opens << [t, nil] }
+ pending_heredocs = []
+ end
+ if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end
+ tok, state, arg = opens.pop
+ opens << [tok, state, arg - 1] if arg >= 1
+ end
+ yield t, opens if block_given?
end
- if t.event == :on_nl || t.event == :on_semicolon
- first_token_on_line = true
- elsif t.event != :on_sp
- first_token_on_line = false
- end
- if pending_heredocs.any? && t.tok.include?("\n")
- pending_heredocs.reverse_each { |t| opens << [t, nil] }
- pending_heredocs = []
- end
- if opens.last && opens.last[1] == :in_alias_undef && !IGNORE_TOKENS.include?(t.event) && t.event != :on_heredoc_end
- tok, state, arg = opens.pop
- opens << [tok, state, arg - 1] if arg >= 1
- end
- yield t, opens if block_given?
+ opens.map(&:first) + pending_heredocs.reverse
end
- opens.map(&:first) + pending_heredocs.reverse
- end
- def self.open_tokens(tokens)
- # scan_opens without block will return a list of open tokens at last token position
- scan_opens(tokens)
- end
+ def open_tokens(tokens)
+ # scan_opens without block will return a list of open tokens at last token position
+ scan_opens(tokens)
+ end
- # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line.
- # Example code
- # ["hello
- # world"+(
- # First line
- # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]]
- # prev_opens: []
- # next_tokens: [lbracket, tstring_beg]
- # min_depth: 0 (minimum at beginning of line)
- # Second line
- # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']]
- # prev_opens: [lbracket, tstring_beg]
- # next_tokens: [lbracket, lparen]
- # min_depth: 1 (minimum just after tstring_end)
- def self.parse_by_line(tokens)
- line_tokens = []
- prev_opens = []
- min_depth = 0
- output = []
- last_opens = scan_opens(tokens) do |t, opens|
- depth = t == opens.last&.first ? opens.size - 1 : opens.size
- min_depth = depth if depth < min_depth
- if t.tok.include?("\n")
- t.tok.each_line do |line|
- line_tokens << [t, line]
- next if line[-1] != "\n"
- next_opens = opens.map(&:first)
- output << [line_tokens, prev_opens, next_opens, min_depth]
- prev_opens = next_opens
- min_depth = prev_opens.size
- line_tokens = []
+ # Calculates token information [line_tokens, prev_opens, next_opens, min_depth] for each line.
+ # Example code
+ # ["hello
+ # world"+(
+ # First line
+ # line_tokens: [[lbracket, '['], [tstring_beg, '"'], [tstring_content("hello\nworld"), "hello\n"]]
+ # prev_opens: []
+ # next_tokens: [lbracket, tstring_beg]
+ # min_depth: 0 (minimum at beginning of line)
+ # Second line
+ # line_tokens: [[tstring_content("hello\nworld"), "world"], [tstring_end, '"'], [op, '+'], [lparen, '(']]
+ # prev_opens: [lbracket, tstring_beg]
+ # next_tokens: [lbracket, lparen]
+ # min_depth: 1 (minimum just after tstring_end)
+ def parse_by_line(tokens)
+ line_tokens = []
+ prev_opens = []
+ min_depth = 0
+ output = []
+ last_opens = scan_opens(tokens) do |t, opens|
+ depth = t == opens.last&.first ? opens.size - 1 : opens.size
+ min_depth = depth if depth < min_depth
+ if t.tok.include?("\n")
+ t.tok.each_line do |line|
+ line_tokens << [t, line]
+ next if line[-1] != "\n"
+ next_opens = opens.map(&:first)
+ output << [line_tokens, prev_opens, next_opens, min_depth]
+ prev_opens = next_opens
+ min_depth = prev_opens.size
+ line_tokens = []
+ end
+ else
+ line_tokens << [t, t.tok]
end
- else
- line_tokens << [t, t.tok]
end
+ output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any?
+ output
end
- output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any?
- output
end
end
end
diff --git a/lib/irb/pager.rb b/lib/irb/pager.rb
index 3391b32c66..558318cdb8 100644
--- a/lib/irb/pager.rb
+++ b/lib/irb/pager.rb
@@ -33,7 +33,11 @@ module IRB
# the `IRB::Abort` exception only interrupts IRB's execution but doesn't affect the pager
# So to properly terminate the pager with Ctrl-C, we need to catch `IRB::Abort` and kill the pager process
rescue IRB::Abort
- Process.kill("TERM", pid) if pid
+ begin
+ Process.kill("TERM", pid) if pid
+ rescue Errno::ESRCH
+ # Pager process already terminated
+ end
nil
rescue Errno::EPIPE
end
diff --git a/lib/irb/ruby-lex.rb b/lib/irb/ruby-lex.rb
index 86e340eb05..3abb53b4ea 100644
--- a/lib/irb/ruby-lex.rb
+++ b/lib/irb/ruby-lex.rb
@@ -36,29 +36,6 @@ module IRB
:massign,
]
- class TerminateLineInput < StandardError
- def initialize
- super("Terminate Line Input")
- end
- end
-
- def self.compile_with_errors_suppressed(code, line_no: 1)
- begin
- result = yield code, line_no
- rescue ArgumentError
- # Ruby can issue an error for the code if there is an
- # incomplete magic comment for encoding in it. Force an
- # expression with a new line before the code in this
- # case to prevent magic comment handling. To make sure
- # line numbers in the lexed code remain the same,
- # decrease the line number by one.
- code = ";\n#{code}"
- line_no -= 1
- result = yield code, line_no
- end
- result
- end
-
ERROR_TOKENS = [
:on_parse_error,
:compile_error,
@@ -68,70 +45,102 @@ module IRB
:on_param_error
]
- def self.generate_local_variables_assign_code(local_variables)
- "#{local_variables.join('=')}=nil;" unless local_variables.empty?
+ LTYPE_TOKENS = %i[
+ on_heredoc_beg on_tstring_beg
+ on_regexp_beg on_symbeg on_backtick
+ on_symbols_beg on_qsymbols_beg
+ on_words_beg on_qwords_beg
+ ]
+
+ class TerminateLineInput < StandardError
+ def initialize
+ super("Terminate Line Input")
+ end
end
- # Some part of the code is not included in Ripper's token.
- # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
- # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
- def self.interpolate_ripper_ignored_tokens(code, tokens)
- line_positions = [0]
- code.lines.each do |line|
- line_positions << line_positions.last + line.bytesize
+ class << self
+ def compile_with_errors_suppressed(code, line_no: 1)
+ begin
+ result = yield code, line_no
+ rescue ArgumentError
+ # Ruby can issue an error for the code if there is an
+ # incomplete magic comment for encoding in it. Force an
+ # expression with a new line before the code in this
+ # case to prevent magic comment handling. To make sure
+ # line numbers in the lexed code remain the same,
+ # decrease the line number by one.
+ code = ";\n#{code}"
+ line_no -= 1
+ result = yield code, line_no
+ end
+ result
+ end
+
+ def generate_local_variables_assign_code(local_variables)
+ "#{local_variables.join('=')}=nil;" unless local_variables.empty?
end
- prev_byte_pos = 0
- interpolated = []
- prev_line = 1
- tokens.each do |t|
- line, col = t.pos
- byte_pos = line_positions[line - 1] + col
- if prev_byte_pos < byte_pos
- tok = code.byteslice(prev_byte_pos...byte_pos)
+
+ # Some part of the code is not included in Ripper's token.
+ # Example: DATA part, token after heredoc_beg when heredoc has unclosed embexpr.
+ # With interpolated tokens, tokens.map(&:tok).join will be equal to code.
+ def interpolate_ripper_ignored_tokens(code, tokens)
+ line_positions = [0]
+ code.lines.each do |line|
+ line_positions << line_positions.last + line.bytesize
+ end
+ prev_byte_pos = 0
+ interpolated = []
+ prev_line = 1
+ tokens.each do |t|
+ line, col = t.pos
+ byte_pos = line_positions[line - 1] + col
+ if prev_byte_pos < byte_pos
+ tok = code.byteslice(prev_byte_pos...byte_pos)
+ pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
+ interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
+ prev_line += tok.count("\n")
+ end
+ interpolated << t
+ prev_byte_pos = byte_pos + t.tok.bytesize
+ prev_line += t.tok.count("\n")
+ end
+ if prev_byte_pos < code.bytesize
+ tok = code.byteslice(prev_byte_pos..)
pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
- prev_line += tok.count("\n")
end
- interpolated << t
- prev_byte_pos = byte_pos + t.tok.bytesize
- prev_line += t.tok.count("\n")
- end
- if prev_byte_pos < code.bytesize
- tok = code.byteslice(prev_byte_pos..)
- pos = [prev_line, prev_byte_pos - line_positions[prev_line - 1]]
- interpolated << Ripper::Lexer::Elem.new(pos, :on_ignored_by_ripper, tok, 0)
+ interpolated
end
- interpolated
- end
- def self.ripper_lex_without_warning(code, local_variables: [])
- verbose, $VERBOSE = $VERBOSE, nil
- lvars_code = generate_local_variables_assign_code(local_variables)
- original_code = code
- if lvars_code
- code = "#{lvars_code}\n#{code}"
- line_no = 0
- else
- line_no = 1
- end
+ def ripper_lex_without_warning(code, local_variables: [])
+ verbose, $VERBOSE = $VERBOSE, nil
+ lvars_code = generate_local_variables_assign_code(local_variables)
+ original_code = code
+ if lvars_code
+ code = "#{lvars_code}\n#{code}"
+ line_no = 0
+ else
+ line_no = 1
+ end
- compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
- lexer = Ripper::Lexer.new(inner_code, '-', line_no)
- tokens = []
- lexer.scan.each do |t|
- next if t.pos.first == 0
- prev_tk = tokens.last
- position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
- if position_overlapped
- tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
- else
- tokens << t
+ compile_with_errors_suppressed(code, line_no: line_no) do |inner_code, line_no|
+ lexer = Ripper::Lexer.new(inner_code, '-', line_no)
+ tokens = []
+ lexer.scan.each do |t|
+ next if t.pos.first == 0
+ prev_tk = tokens.last
+ position_overlapped = prev_tk && t.pos[0] == prev_tk.pos[0] && t.pos[1] < prev_tk.pos[1] + prev_tk.tok.bytesize
+ if position_overlapped
+ tokens[-1] = t if ERROR_TOKENS.include?(prev_tk.event) && !ERROR_TOKENS.include?(t.event)
+ else
+ tokens << t
+ end
end
+ interpolate_ripper_ignored_tokens(original_code, tokens)
end
- interpolate_ripper_ignored_tokens(original_code, tokens)
+ ensure
+ $VERBOSE = verbose
end
- ensure
- $VERBOSE = verbose
end
def check_code_state(code, local_variables:)
@@ -219,27 +228,6 @@ module IRB
:unrecoverable_error
rescue SyntaxError => e
case e.message
- when /unterminated (?:string|regexp) meets end of file/
- # "unterminated regexp meets end of file"
- #
- # example:
- # /
- #
- # "unterminated string meets end of file"
- #
- # example:
- # '
- return :recoverable_error
- when /unexpected end-of-input/
- # "syntax error, unexpected end-of-input, expecting keyword_end"
- #
- # example:
- # if true
- # hoge
- # if false
- # fuga
- # end
- return :recoverable_error
when /unexpected keyword_end/
# "syntax error, unexpected keyword_end"
#
@@ -262,6 +250,27 @@ module IRB
# example:
# method / f /
return :unrecoverable_error
+ when /unterminated (?:string|regexp) meets end of file/
+ # "unterminated regexp meets end of file"
+ #
+ # example:
+ # /
+ #
+ # "unterminated string meets end of file"
+ #
+ # example:
+ # '
+ return :recoverable_error
+ when /unexpected end-of-input/
+ # "syntax error, unexpected end-of-input, expecting keyword_end"
+ #
+ # example:
+ # if true
+ # hoge
+ # if false
+ # fuga
+ # end
+ return :recoverable_error
else
return :other_error
end
@@ -391,13 +400,6 @@ module IRB
end
end
- LTYPE_TOKENS = %i[
- on_heredoc_beg on_tstring_beg
- on_regexp_beg on_symbeg on_backtick
- on_symbols_beg on_qsymbols_beg
- on_words_beg on_qwords_beg
- ]
-
def ltype_from_open_tokens(opens)
start_token = opens.reverse_each.find do |tok|
LTYPE_TOKENS.include?(tok.event)
diff --git a/lib/irb/ruby_logo.aa b/lib/irb/ruby_logo.aa
index 61fe22c94a..d0143a448b 100644
--- a/lib/irb/ruby_logo.aa
+++ b/lib/irb/ruby_logo.aa
@@ -1,41 +1,41 @@
-TYPE: LARGE
+TYPE: ASCII_LARGE
- -+smJYYN?mm-
- HB"BBYT TQg NggT
- 9Q+g Nm,T 8g NJW
- YS+ N2NJ"Sg N?
- BQg #( gT Nggggk J
- 5j NJ NJ NNge
- #Q #JJ NgT N(
- @j bj mT J
- Bj @/d NJ (
- #q #(( NgT #J
- 5d #(t mT $d
- #q @(@J NJB;
- @( 5d ? HHH H HQmgggggggmN qD
- 5d #uN 2QdH E O
- 5 5JSd Nd NJH @d j
- Fd @J4d s NQH #d (
- #( #o6d Nd NgH #d #d
- 4 B&Od v NgT #d F
- #( 9JGd NH NgUd F
- #d #GJQ d NP $
- #J #U+#Q N Q # j
- j /W BQ+ BQ d NJ NJ
- - NjJH HBIjTQggPJQgW N W k #J
- #J b HYWgggN j s Nag d NN b #d
- #J 5- D s Ngg N d Nd F
- Fd BKH2 #+ s NNgg J Q J ]
- F H @ J N y K(d P I
- F4 E N? #d y #Q NJ E j
- F W Nd q m Bg NxW N(H-
- F d b @ m Hd gW vKJ
- NJ d K d s Bg aT FDd
- b # d N m BQ mV N>
- e5 Nd #d NggggggQWH HHHH NJ -
- m7 NW H N HSVO1z=?11-
- NgTH bB kH WBHWWHBHWmQgg&gggggNNN
- NNggggggNN
+ ,,,;;;;;;;;;;;;;;;;;;;;;;,,
+ ,,,;;;;;;;;;,, ,;;;' ''';;,
+ ,,;;;''' ';;;, ,,;;'' '';,
+ ,;;'' ;;;;;;;;,,,,,, ';;
+ ,;;'' ;;;;';;;'''';;;;;;;;;,,,;;
+ ,,;'' ;;;; ';;, ''''';;,
+ ,;;' ;;;' ';;, ;;
+ ,;;' ,;;; '';,, ;;
+ ,;;' ;;; ';;, ,;;
+ ;;' ;;;' '';,, ;;;
+ ,;' ;;;; ';;, ;;'
+ ,;;' ,;;;;' ,,,,,,,,,,,,;;;;;
+ ,;' ,;;;;;;;;;;;;;;;;;;;;'''''''';;;
+ ;;' ,;;;;;;;;;,, ;;;;
+ ;;' ,;;;'' ;;, ';;,, ,;;;;
+ ;;' ,;;;' ;; '';;, ,;';;;
+ ;;' ,;;;' ;;, '';;,, ,;',;;;
+ ,;;; ,;;;' ;; '';;,, ,;' ;;;'
+ ;;;; ,,;;;' ;;, ';;;' ;;;
+,;;; ,;;;;' ;; ,;;; ;;;
+;;;;; ,,;;;;;' ;;, ,;';; ;;;
+;;;;;, ,,;;;;;;;' ;; ,;;' ;;; ;;;
+;;;;;;;,,,,,,,;;;;;;;;;;;;;;,,, ;;, ,;' ;; ;;;
+;;' ;;;;;;;;;;'''' ,;';; ''';;;;,,, ;; ,;; ;; ;;;
+;; ;;;'' ;; ';; ''';;;;,,,, ;;, ,;;' ;;, ;;
+;; ;;;;, ;;' ';; ''';;;;,,;;;;' ';; ;;
+;;;;;;';, ,;; ;; '';;;;, ;;,;;
+;;; ;; ;;, ;; ;; ,;;' ';;, ;;;;;
+;; ;;; ;;, ;;' ;; ,,;'' ';;, ;;;;;
+;; ;; ;; ;; ;; ,;;' '';, ;;;;
+;;,;; ;; ;;' ;; ,;;'' ';,, ;;;'
+ ;;;; ';; ,;; ;;,,;;'' ';;, ;;;
+ ';;; ';; ;; ,;;;;;;;;;;;;;,,,,,,,,,,,, ';;;;;
+ ';, ';,;;' ,,,;;'' '''''''';;;;;;;;;;;;;;;;;;;
+ ';;,,, ;;;; ,,,,;;;;;;,,,,,;;;;;;;;;;;;;;;;;;;''''''''''''''
+ ''';;;;;;;;;;;;;;'''''''''''''''
TYPE: ASCII
,,,;;;;''''';;;'';,
,,;'' ';;,;;; ',
@@ -57,6 +57,44 @@ TYPE: ASCII
;;; '; ;' ';,,'' ';,;;
'; ';,; ,,;''''''''';;;;;;,,;;;
';,,;;,,;;;;;;;;;;''''''''''''''
+TYPE: UNICODE_LARGE
+
+ ⣀⣤⣴⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣤⣄⡀
+ ⢀⣀⣤⣴⣾⣿⣿⣿⠿⣿⣿⣿⣿⣦⣀ ⢀⣤⣶⣿⠿⠛⠁⠈⠉⠙⠻⢿⣷⣦⡀
+ ⢀⣠⣴⣾⡿⠿⠛⠉⠉ ⠈⠙⢿⣿⣷⣤⡀ ⣠⣴⣾⡿⠟⠉ ⠉⠻⣿⣦
+ ⢀⣤⣶⣿⠟⠋⠁ ⢿⣿⣿⣿⣿⣿⣿⣧⣤⣤⣤⣀⣀⣀⡀ ⠘⢿⣷⡀
+ ⢀⣠⣾⡿⠟⠉ ⢸⣿⣿⣿⠟⢿⣿⣯⡙⠛⠛⠛⠿⠿⠿⢿⣿⣿⣶⣶⣶⣦⣤⣬⣿⣧
+ ⣠⣴⣿⠟⠋ ⢸⣿⣿⡿ ⠈⠻⣿⣶⣄ ⠉⠉⠉⠙⠛⢻⣿⡆
+ ⣠⣾⡿⠛⠁ ⣼⣿⣿⠃ ⠈⠙⢿⣷⣤⡀ ⠈⣿⡇
+ ⣠⣾⡿⠋ ⢠⣿⣿⡏ ⠙⠻⣿⣦⣀ ⣿⡇
+ ⣠⣾⡿⠋ ⢀⣿⣿⣿ ⠈⠛⢿⣷⣄⡀ ⢠⣿⡇
+ ⢀⣾⡿⠋ ⢀⣾⣿⣿⠇ ⠙⠻⣿⣦⣀ ⢸⣿⡇
+ ⢀⣴⣿⠟⠁ ⢀⣾⣿⣿⡟ ⠈⠻⢿⣷⣄ ⣾⣿⠇
+ ⢠⣾⡿⠃ ⣠⣿⣿⣿⣿⠃ ⣀⣀⣀⣀⣀⣀⣀⣀⣤⣤⣤⣤⣽⣿⣿⣿⣿
+ ⣰⣿⠟ ⣴⣿⣿⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠿⠛⠛⠛⠛⠛⠛⠛⠛⣿⣿⣿
+ ⣼⣿⠏ ⢠⣾⣿⣿⣿⡿⣿⣿⢿⣷⣦⣄ ⣼⣿⣿⣿
+ ⣼⣿⠃ ⢀⣴⣿⣿⣿⠟⠋ ⢸⣿⡆⠈⠛⠿⣿⣦⣄⡀ ⣰⣿⣿⣿⡇
+ ⢀⣾⣿⠃ ⢀⣴⣿⣿⣿⠟⠁ ⣿⣷ ⠈⠙⠻⣿⣶⣄⡀ ⣰⣿⠟⣿⣿⡇
+ ⢀⣾⣿⠇ ⢀⣴⣿⣿⣿⠟⠁ ⢸⣿⡆ ⠙⠻⢿⣷⣤⣀ ⣰⣿⠏⢠⣿⣿⡇
+ ⢠⣿⣿⡟ ⢀⣴⣿⣿⡿⠛⠁ ⣿⣷ ⠉⠻⢿⣷⣦⣀ ⣴⣿⠏ ⢸⣿⣿⠃
+ ⣿⣿⣿⡇ ⣠⣴⣿⣿⡿⠋ ⢸⣿⡆ ⠈⠛⢿⣿⣿⠃ ⢸⣿⣿
+⢠⣿⣿⣿ ⢀⣴⣾⣿⣿⡿⠋ ⠈⣿⣧ ⢠⣾⣿⣿ ⢸⣿⣿
+⢸⣿⣿⣿⡇ ⣀⣴⣾⣿⣿⣿⡿⠋ ⢹⣿⡆ ⣴⣿⠟⢹⣿⡀ ⢸⣿⡿
+⢸⣿⡟⣿⣿⣄ ⣀⣤⣶⣿⣿⣿⣿⣿⡟⠉ ⠈⣿⣷ ⢠⣾⡿⠋ ⢸⣿⡇ ⣼⣿⡇
+⢸⣿⡇⢹⣿⣿⣷⣦⣤⣤⣤⣤⣤⣴⣶⣾⣿⣿⣿⣿⡿⠿⣿⣿⣿⣿⣷⣶⣤⣤⣀⡀ ⢹⣿⡆ ⢀⣴⣿⠟ ⣿⣧ ⣿⣿⡇
+⢸⣿⠃ ⢿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠛⠛⠉⠉⠁ ⢰⣿⠟⣿⣷⡀⠉⠙⠛⠿⢿⣿⣶⣦⣤⣀⡀ ⠈⣿⣷ ⣠⣿⡿⠁ ⢿⣿ ⣿⣿⡇
+⢸⣿ ⢀⣾⣿⣿⠋⠉⠁ ⢀⣿⡿ ⠘⣿⣷⡀ ⠉⠙⠛⠿⠿⣿⣶⣦⣤⣄⣀ ⢹⣿⡄ ⣠⣾⡿⠋ ⢸⣿⡆ ⣿⣿
+⣸⣿⢀⣾⣿⣿⣿⣆ ⣸⣿⠃ ⠘⢿⣷⡀ ⠈⠉⠛⠻⠿⣿⣷⣶⣤⣌⣿⣷⣾⡿⠋ ⠘⣿⡇ ⣿⣿
+⣿⣿⣾⡿⣿⡿⠹⣿⡆ ⢠⣿⡏ ⠈⢿⣷⡀ ⠈⠉⠙⣻⣿⣿⣿⣀ ⣿⣷⢰⣿⣿
+⣿⣿⡿⢁⣿⡇ ⢻⣿⡄ ⣾⣿ ⠈⢿⣷⡀ ⢀⣤⣾⡿⠋⠈⠻⢿⣷⣄ ⢻⣿⢸⣿⡟
+⣿⣿⠁⢸⣿⡇ ⢻⣿⡄ ⢸⣿⠇ ⠈⢿⣷⡀ ⣀⣴⣿⠟⠋ ⠙⢿⣷⣤⡀ ⢸⣿⣿⣿⡇
+⣿⣿ ⢸⣿⠁ ⠈⢿⣷⡀ ⢀⣿⡟ ⠈⢿⣷⡀ ⢀⣤⣾⡿⠛⠁ ⠙⠻⣿⣦⡀ ⠈⣿⣿⣿⡇
+⢸⣿⡄⣿⣿ ⠈⣿⣷⡀ ⣼⣿⠃ ⠈⢿⣷⡀ ⢀⣠⣶⣿⠟⠋ ⠈⠻⣿⣦⣄ ⣿⣿⣿⠇
+⠈⣿⣷⣿⡿ ⠘⣿⣧ ⢠⣿⡏ ⠈⢿⣷⣄⣤⣶⣿⠟⠋ ⠈⠛⢿⣷⣄ ⢸⣿⣿
+ ⠘⣿⣿⡇ ⠘⣿⣧ ⣾⣿ ⢀⣠⣼⣿⣿⣿⣿⣿⣷⣶⣶⣶⣶⣶⣶⣤⣤⣤⣤⣤⣤⣀⣀⣀⣀⣀⣀⡀ ⠙⢿⣷⣼⣿⣿
+ ⠈⠻⣿⣦⡀ ⠹⣿⣆⢸⣿⠇ ⣀⣠⣴⣾⡿⠟⠋⠁ ⠉⠉⠉⠉⠉⠉⠛⠛⣛⣛⣛⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⡿
+ ⠈⠻⢿⣷⣦⣄⣀⡀ ⢹⣿⣿⡟ ⢀⣀⣀⣤⣤⣶⣾⣿⣿⣿⣯⣥⣤⣤⣤⣤⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠟⠛⠛⠛⠛⠛⠛⠛⠉⠉⠉⠉⠉⠉
+ ⠉⠙⠛⠿⠿⠿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⠿⠛⠛⠛⠛⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠉
TYPE: UNICODE
⣀⣤⣴⣾⣿⣿⣿⡛⠛⠛⠛⠛⣻⣿⠿⠛⠛⠶⣤⡀
⣀⣴⠾⠛⠉⠁ ⠙⣿⣶⣤⣶⣟⣉ ⠈⠻⣦
diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb
index 5d7d729d19..c515da5702 100644
--- a/lib/irb/source_finder.rb
+++ b/lib/irb/source_finder.rb
@@ -100,7 +100,7 @@ module IRB
Source.new(file, line)
elsif method
# Method defined with eval, probably in IRB session
- source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
+ source = RubyVM::InstructionSequence.of(method)&.script_lines&.join rescue nil
Source.new(file, line, source)
end
rescue EvaluationError
diff --git a/lib/irb/statement.rb b/lib/irb/statement.rb
index a3391c12a3..9591a40357 100644
--- a/lib/irb/statement.rb
+++ b/lib/irb/statement.rb
@@ -68,7 +68,7 @@ module IRB
end
def suppresses_echo?
- false
+ true
end
def should_be_handled_by_debugger?
diff --git a/lib/irb/version.rb b/lib/irb/version.rb
index c41917329c..e935a1d7f7 100644
--- a/lib/irb/version.rb
+++ b/lib/irb/version.rb
@@ -5,7 +5,7 @@
#
module IRB # :nodoc:
- VERSION = "1.13.1"
+ VERSION = "1.14.0"
@RELEASE_VERSION = VERSION
- @LAST_UPDATE_DATE = "2024-05-05"
+ @LAST_UPDATE_DATE = "2024-07-06"
end
diff --git a/lib/irb/workspace.rb b/lib/irb/workspace.rb
index d24d1cc38d..632b432439 100644
--- a/lib/irb/workspace.rb
+++ b/lib/irb/workspace.rb
@@ -176,11 +176,13 @@ EOF
end
module HelpersContainer
- def self.install_helper_methods
- HelperMethod.helper_methods.each do |name, helper_method_class|
- define_method name do |*args, **opts, &block|
- helper_method_class.instance.execute(*args, **opts, &block)
- end unless method_defined?(name)
+ class << self
+ def install_helper_methods
+ HelperMethod.helper_methods.each do |name, helper_method_class|
+ define_method name do |*args, **opts, &block|
+ helper_method_class.instance.execute(*args, **opts, &block)
+ end unless method_defined?(name)
+ end
end
end
diff --git a/lib/logger.rb b/lib/logger.rb
index 4099955ef2..63179ec671 100644
--- a/lib/logger.rb
+++ b/lib/logger.rb
@@ -381,7 +381,7 @@ class Logger
# Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
def level
- @level_override[Fiber.current] || @level
+ level_override[Fiber.current] || @level
end
# Sets the log level; returns +severity+.
@@ -406,14 +406,14 @@ class Logger
# logger.debug { "Hello" }
# end
def with_level(severity)
- prev, @level_override[Fiber.current] = level, Severity.coerce(severity)
+ prev, level_override[Fiber.current] = level, Severity.coerce(severity)
begin
yield
ensure
if prev
- @level_override[Fiber.current] = prev
+ level_override[Fiber.current] = prev
else
- @level_override.delete(Fiber.current)
+ level_override.delete(Fiber.current)
end
end
end
@@ -574,10 +574,14 @@ class Logger
# - +shift_period_suffix+: sets the format for the filename suffix
# for periodic log file rotation; default is <tt>'%Y%m%d'</tt>.
# See {Periodic Rotation}[rdoc-ref:Logger@Periodic+Rotation].
+ # - +reraise_write_errors+: An array of exception classes, which will
+ # be reraised if there is an error when writing to the log device.
+ # The default is to swallow all exceptions raised.
#
def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
progname: nil, formatter: nil, datetime_format: nil,
- binmode: false, shift_period_suffix: '%Y%m%d')
+ binmode: false, shift_period_suffix: '%Y%m%d',
+ reraise_write_errors: [])
self.level = level
self.progname = progname
@default_formatter = Formatter.new
@@ -589,7 +593,8 @@ class Logger
@logdev = LogDevice.new(logdev, shift_age: shift_age,
shift_size: shift_size,
shift_period_suffix: shift_period_suffix,
- binmode: binmode)
+ binmode: binmode,
+ reraise_write_errors: reraise_write_errors)
end
end
@@ -741,6 +746,11 @@ private
SEV_LABEL[severity] || 'ANY'
end
+ # Guarantee the existence of this ivar even when subclasses don't call the superclass constructor.
+ def level_override
+ @level_override ||= {}
+ end
+
def format_message(severity, datetime, progname, msg)
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
end
diff --git a/lib/logger/log_device.rb b/lib/logger/log_device.rb
index 84277a2656..4876adf0b7 100644
--- a/lib/logger/log_device.rb
+++ b/lib/logger/log_device.rb
@@ -11,9 +11,10 @@ class Logger
attr_reader :filename
include MonitorMixin
- def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false)
+ def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false, reraise_write_errors: [])
@dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
@binmode = binmode
+ @reraise_write_errors = reraise_write_errors
mon_initialize
set_dev(log)
if @filename
@@ -34,16 +35,22 @@ class Logger
if @shift_age and @dev.respond_to?(:stat)
begin
check_shift_log
+ rescue *@reraise_write_errors
+ raise
rescue
warn("log shifting failed. #{$!}")
end
end
begin
@dev.write(message)
+ rescue *@reraise_write_errors
+ raise
rescue
warn("log writing failed. #{$!}")
end
end
+ rescue *@reraise_write_errors
+ raise
rescue Exception => ignored
warn("log writing failed. #{ignored}")
end
diff --git a/lib/logger/period.rb b/lib/logger/period.rb
index 0a291dbbbe..a0359defe3 100644
--- a/lib/logger/period.rb
+++ b/lib/logger/period.rb
@@ -8,14 +8,14 @@ class Logger
def next_rotate_time(now, shift_age)
case shift_age
- when 'daily'
+ when 'daily', :daily
t = Time.mktime(now.year, now.month, now.mday) + SiD
- when 'weekly'
+ when 'weekly', :weekly
t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
- when 'monthly'
+ when 'monthly', :monthly
t = Time.mktime(now.year, now.month, 1) + SiD * 32
return Time.mktime(t.year, t.month, 1)
- when 'now', 'everytime'
+ when 'now', 'everytime', :now, :everytime
return now
else
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
@@ -30,13 +30,13 @@ class Logger
def previous_period_end(now, shift_age)
case shift_age
- when 'daily'
+ when 'daily', :daily
t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
- when 'weekly'
+ when 'weekly', :weekly
t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
- when 'monthly'
+ when 'monthly', :monthly
t = Time.mktime(now.year, now.month, 1) - SiD / 2
- when 'now', 'everytime'
+ when 'now', 'everytime', :now, :everytime
return now
else
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
diff --git a/lib/logger/version.rb b/lib/logger/version.rb
index 202b6e4fba..2a0801be63 100644
--- a/lib/logger/version.rb
+++ b/lib/logger/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class Logger
- VERSION = "1.6.0"
+ VERSION = "1.6.1"
end
diff --git a/lib/net/http.rb b/lib/net/http.rb
index 6b78c264af..5cc9d2ce88 100644
--- a/lib/net/http.rb
+++ b/lib/net/http.rb
@@ -67,6 +67,8 @@ module Net #:nodoc:
# Net::HTTP.post(uri, data)
# params = {title: 'foo', body: 'bar', userId: 1}
# Net::HTTP.post_form(uri, params)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Net::HTTP.put(uri, data)
#
# - If performance is important, consider using sessions, which lower request overhead.
# This {session}[rdoc-ref:Net::HTTP@Sessions] has multiple requests for
@@ -524,6 +526,8 @@ module Net #:nodoc:
# Sends a POST request with form data and returns a response object.
# - {::post}[rdoc-ref:Net::HTTP.post]:
# Sends a POST request with data and returns a response object.
+ # - {::put}[rdoc-ref:Net::HTTP.put]:
+ # Sends a PUT request with data and returns a response object.
# - {#copy}[rdoc-ref:Net::HTTP#copy]:
# Sends a COPY request and returns a response object.
# - {#delete}[rdoc-ref:Net::HTTP#delete]:
@@ -893,6 +897,39 @@ module Net #:nodoc:
}
end
+ # Sends a PUT request to the server; returns a Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Net::HTTP.put(_uri, data, headers) # => #<Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Net::HTTP::Put: request class for \HTTP method +PUT+.
+ # - Net::HTTP#put: convenience method for \HTTP method +PUT+.
+ #
+ def HTTP.put(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.put(url, data, header)
+ }
+ end
+
#
# \HTTP session management
#
@@ -1066,7 +1103,7 @@ module Net #:nodoc:
# For proxy-defining arguments +p_addr+ through +p_no_proxy+,
# see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
#
- def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil)
+ def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil)
http = super address, port
if proxy_class? then # from Net::HTTP::Proxy()
@@ -1075,6 +1112,7 @@ module Net #:nodoc:
http.proxy_port = @proxy_port
http.proxy_user = @proxy_user
http.proxy_pass = @proxy_pass
+ http.proxy_use_ssl = @proxy_use_ssl
elsif p_addr == :ENV then
http.proxy_from_env = true
else
@@ -1086,34 +1124,67 @@ module Net #:nodoc:
http.proxy_port = p_port || default_port
http.proxy_user = p_user
http.proxy_pass = p_pass
+ http.proxy_use_ssl = p_use_ssl
end
http
end
+ class << HTTP
+ # Allows to set the default configuration that will be used
+ # when creating a new connection.
+ #
+ # Example:
+ #
+ # Net::HTTP.default_configuration = {
+ # read_timeout: 1,
+ # write_timeout: 1
+ # }
+ # http = Net::HTTP.new(hostname)
+ # http.open_timeout # => 60
+ # http.read_timeout # => 1
+ # http.write_timeout # => 1
+ #
+ attr_accessor :default_configuration
+ end
+
# Creates a new \Net::HTTP object for the specified server address,
# without opening the TCP connection or initializing the \HTTP session.
# The +address+ should be a DNS hostname or IP address.
def initialize(address, port = nil) # :nodoc:
+ defaults = {
+ keep_alive_timeout: 2,
+ close_on_empty_response: false,
+ open_timeout: 60,
+ read_timeout: 60,
+ write_timeout: 60,
+ continue_timeout: nil,
+ max_retries: 1,
+ debug_output: nil,
+ response_body_encoding: false,
+ ignore_eof: true
+ }
+ options = defaults.merge(self.class.default_configuration || {})
+
@address = address
@port = (port || HTTP.default_port)
@ipaddr = nil
@local_host = nil
@local_port = nil
@curr_http_version = HTTPVersion
- @keep_alive_timeout = 2
+ @keep_alive_timeout = options[:keep_alive_timeout]
@last_communicated = nil
- @close_on_empty_response = false
+ @close_on_empty_response = options[:close_on_empty_response]
@socket = nil
@started = false
- @open_timeout = 60
- @read_timeout = 60
- @write_timeout = 60
- @continue_timeout = nil
- @max_retries = 1
- @debug_output = nil
- @response_body_encoding = false
- @ignore_eof = true
+ @open_timeout = options[:open_timeout]
+ @read_timeout = options[:read_timeout]
+ @write_timeout = options[:write_timeout]
+ @continue_timeout = options[:continue_timeout]
+ @max_retries = options[:max_retries]
+ @debug_output = options[:debug_output]
+ @response_body_encoding = options[:response_body_encoding]
+ @ignore_eof = options[:ignore_eof]
@proxy_from_env = false
@proxy_uri = nil
@@ -1121,6 +1192,7 @@ module Net #:nodoc:
@proxy_port = nil
@proxy_user = nil
@proxy_pass = nil
+ @proxy_use_ssl = nil
@use_ssl = false
@ssl_context = nil
@@ -1255,6 +1327,7 @@ module Net #:nodoc:
# Sets the proxy password;
# see {Proxy Server}[rdoc-ref:Net::HTTP@Proxy+Server].
attr_writer :proxy_pass
+ attr_writer :proxy_use_ssl
# Returns the IP address for the connection.
#
@@ -1444,23 +1517,6 @@ module Net #:nodoc:
@use_ssl = flag
end
- SSL_IVNAMES = [
- :@ca_file,
- :@ca_path,
- :@cert,
- :@cert_store,
- :@ciphers,
- :@extra_chain_cert,
- :@key,
- :@ssl_timeout,
- :@ssl_version,
- :@min_version,
- :@max_version,
- :@verify_callback,
- :@verify_depth,
- :@verify_mode,
- :@verify_hostname,
- ] # :nodoc:
SSL_ATTRIBUTES = [
:ca_file,
:ca_path,
@@ -1479,6 +1535,8 @@ module Net #:nodoc:
:verify_hostname,
] # :nodoc:
+ SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym } # :nodoc:
+
# Sets or returns the path to a CA certification file in PEM format.
attr_accessor :ca_file
@@ -1614,7 +1672,13 @@ module Net #:nodoc:
debug "opened"
if use_ssl?
if proxy?
- plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
+ if @proxy_use_ssl
+ proxy_sock = OpenSSL::SSL::SSLSocket.new(s)
+ ssl_socket_connect(proxy_sock, @open_timeout)
+ else
+ proxy_sock = s
+ end
+ proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout,
write_timeout: @write_timeout,
continue_timeout: @continue_timeout,
debug_output: @debug_output)
@@ -1625,8 +1689,8 @@ module Net #:nodoc:
buf << "Proxy-Authorization: Basic #{credential}\r\n"
end
buf << "\r\n"
- plain_sock.write(buf)
- HTTPResponse.read_new(plain_sock).value
+ proxy_sock.write(buf)
+ HTTPResponse.read_new(proxy_sock).value
# assuming nothing left in buffers after successful CONNECT response
end
@@ -1734,13 +1798,14 @@ module Net #:nodoc:
@proxy_port = nil
@proxy_user = nil
@proxy_pass = nil
+ @proxy_use_ssl = nil
# Creates an \HTTP proxy class which behaves like \Net::HTTP, but
# performs all access via the specified proxy.
#
# This class is obsolete. You may pass these same parameters directly to
# \Net::HTTP.new. See Net::HTTP.new for details of the arguments.
- def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) #:nodoc:
+ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc:
return self unless p_addr
Class.new(self) {
@@ -1758,6 +1823,7 @@ module Net #:nodoc:
@proxy_user = p_user
@proxy_pass = p_pass
+ @proxy_use_ssl = p_use_ssl
}
end
@@ -1782,6 +1848,9 @@ module Net #:nodoc:
# Returns the password for accessing the proxy, or +nil+ if none;
# see Net::HTTP@Proxy+Server.
attr_reader :proxy_pass
+
+ # Use SSL when talking to the proxy. If Net::HTTP does not use a proxy, nil.
+ attr_reader :proxy_use_ssl
end
# Returns +true+ if a proxy server is defined, +false+ otherwise;
@@ -2016,6 +2085,11 @@ module Net #:nodoc:
# http = Net::HTTP.new(hostname)
# http.put('/todos/1', data) # => #<Net::HTTPOK 200 OK readbody=true>
#
+ # Related:
+ #
+ # - Net::HTTP::Put: request class for \HTTP method PUT.
+ # - Net::HTTP.put: sends PUT request, returns response body.
+ #
def put(path, data, initheader = nil)
request(Put.new(path, initheader), data)
end
diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb
index 6660c8075a..f6c36f1b5e 100644
--- a/lib/net/http/header.rb
+++ b/lib/net/http/header.rb
@@ -491,7 +491,7 @@ module Net::HTTPHeader
alias canonical_each each_capitalized
def capitalize(name)
- name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
end
private :capitalize
diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb
index 5724164205..e58057adf1 100644
--- a/lib/net/http/requests.rb
+++ b/lib/net/http/requests.rb
@@ -124,6 +124,11 @@ end
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
+# Related:
+#
+# - Net::HTTP.put: sends +PUT+ request, returns response object.
+# - Net::HTTP#put: sends +PUT+ request, returns response object.
+#
class Net::HTTP::Put < Net::HTTPRequest
METHOD = 'PUT'
REQUEST_HAS_BODY = true
diff --git a/lib/open-uri.rb b/lib/open-uri.rb
index ba2379325f..f2eddbcd2b 100644
--- a/lib/open-uri.rb
+++ b/lib/open-uri.rb
@@ -109,6 +109,7 @@ module OpenURI
:redirect => true,
:encoding => nil,
:max_redirects => 64,
+ :request_specific_fields => nil,
}
def OpenURI.check_options(options) # :nodoc:
@@ -148,7 +149,11 @@ module OpenURI
end
encoding = Encoding.find(options[:encoding])
end
-
+ if options.has_key? :request_specific_fields
+ if !(options[:request_specific_fields].is_a?(Hash) || options[:request_specific_fields].is_a?(Proc))
+ raise ArgumentError, "Invalid request_specific_fields option: #{options[:request_specific_fields].inspect}"
+ end
+ end
unless mode == nil ||
mode == 'r' || mode == 'rb' ||
mode == File::RDONLY
@@ -212,12 +217,20 @@ module OpenURI
end
uri_set = {}
- max_redirects = options[:max_redirects]
+ max_redirects = options[:max_redirects] || Options.fetch(:max_redirects)
buf = nil
while true
+ request_specific_fields = {}
+ if options.has_key? :request_specific_fields
+ request_specific_fields = if options[:request_specific_fields].is_a?(Hash)
+ options[:request_specific_fields]
+ else options[:request_specific_fields].is_a?(Proc)
+ options[:request_specific_fields].call(uri)
+ end
+ end
redirect = catch(:open_uri_redirect) {
buf = Buffer.new
- uri.buffer_open(buf, find_proxy.call(uri), options)
+ uri.buffer_open(buf, find_proxy.call(uri), options.merge(request_specific_fields))
nil
}
if redirect
@@ -237,6 +250,10 @@ module OpenURI
options = options.dup
options.delete :http_basic_authentication
end
+ if options.include?(:request_specific_fields) && options[:request_specific_fields].is_a?(Hash)
+ # Send request specific headers only for the initial request.
+ options.delete :request_specific_fields
+ end
uri = redirect
raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s
uri_set[uri.to_s] = true
@@ -746,6 +763,44 @@ module OpenURI
# Using +true+ also means that redirections between http and ftp are
# permitted.
#
+ # [:max_redirects]
+ # Synopsis:
+ # :max_redirects=>int
+ #
+ # Number of HTTP redirects allowed before OpenURI::TooManyRedirects is raised.
+ # The default is 64.
+ #
+ # [:request_specific_fields]
+ # Synopsis:
+ # :request_specific_fields => {}
+ # :request_specific_fields => lambda {|url| ...}
+ #
+ # :request_specific_fields option allows specifying custom header fields that
+ # are sent with the HTTP request. It can be passed as a Hash or a Proc that
+ # gets evaluated on each request and returns a Hash of header fields.
+ #
+ # If a Hash is provided, it specifies the headers only for the initial
+ # request and these headers will not be sent on redirects.
+ #
+ # If a Proc is provided, it will be executed for each request including
+ # redirects, allowing dynamic header customization based on the request URL.
+ # It is important that the Proc returns a Hash. And this Hash specifies the
+ # headers to be sent with the request.
+ #
+ # For Example with Hash
+ # URI.open("http://...",
+ # request_specific_fields: {"Authorization" => "token dummy"}) {|f| ... }
+ #
+ # For Example with Proc:
+ # URI.open("http://...",
+ # request_specific_fields: lambda { |uri|
+ # if uri.host == "example.com"
+ # {"Authorization" => "token dummy"}
+ # else
+ # {}
+ # end
+ # }) {|f| ... }
+ #
def open(*rest, &block)
OpenURI.open_uri(self, *rest, &block)
end
diff --git a/lib/optparse.rb b/lib/optparse.rb
index 069c3e436e..50641867f0 100644
--- a/lib/optparse.rb
+++ b/lib/optparse.rb
@@ -1115,7 +1115,7 @@ XXX
Switch::OptionalArgument.new do |pkg|
if pkg
begin
- require 'optparse/version'
+ require_relative 'optparse/version'
rescue LoadError
else
show_version(*pkg.split(/,/)) or
@@ -1729,9 +1729,9 @@ XXX
end
end
begin
- opt, cb, *val = sw.parse(rest, argv) {|*exc| raise(*exc)}
- val = callback!(cb, 1, *val) if cb
- callback!(setter, 2, sw.switch_name, *val) if setter
+ opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
rescue ParseError
raise $!.set_option(arg, rest)
end
@@ -1761,7 +1761,7 @@ XXX
raise $!.set_option(arg, true)
end
begin
- opt, cb, *val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
+ opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
rescue ParseError
raise $!.set_option(arg, arg.length > 2)
else
@@ -1769,8 +1769,8 @@ XXX
end
begin
argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
- val = callback!(cb, 1, *val) if cb
- callback!(setter, 2, sw.switch_name, *val) if setter
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
rescue ParseError
raise $!.set_option(arg, arg.length > 2)
end
@@ -1798,6 +1798,8 @@ XXX
# Calls callback with _val_.
def callback!(cb, max_arity, *args) # :nodoc:
+ args.compact!
+
if (size = args.size) < max_arity and cb.to_proc.lambda?
(arity = cb.arity) < 0 and arity = (1-arity)
arity = max_arity if arity > max_arity
diff --git a/lib/prism.rb b/lib/prism.rb
index 2bb7f79bf6..66a64e7fd0 100644
--- a/lib/prism.rb
+++ b/lib/prism.rb
@@ -13,7 +13,6 @@ module Prism
autoload :BasicVisitor, "prism/visitor"
autoload :Compiler, "prism/compiler"
- autoload :Debug, "prism/debug"
autoload :DesugarCompiler, "prism/desugar_compiler"
autoload :Dispatcher, "prism/dispatcher"
autoload :DotVisitor, "prism/dot_visitor"
@@ -32,7 +31,6 @@ module Prism
# Some of these constants are not meant to be exposed, so marking them as
# private here.
- private_constant :Debug
private_constant :LexCompat
private_constant :LexRipper
diff --git a/lib/prism/debug.rb b/lib/prism/debug.rb
deleted file mode 100644
index 74f824faa7..0000000000
--- a/lib/prism/debug.rb
+++ /dev/null
@@ -1,249 +0,0 @@
-# frozen_string_literal: true
-
-module Prism
- # This module is used for testing and debugging and is not meant to be used by
- # consumers of this library.
- module Debug
- # A wrapper around a RubyVM::InstructionSequence that provides a more
- # convenient interface for accessing parts of the iseq.
- class ISeq # :nodoc:
- attr_reader :parts
-
- def initialize(parts)
- @parts = parts
- end
-
- def type
- parts[0]
- end
-
- def local_table
- parts[10]
- end
-
- def instructions
- parts[13]
- end
-
- def each_child
- instructions.each do |instruction|
- # Only look at arrays. Other instructions are line numbers or
- # tracepoint events.
- next unless instruction.is_a?(Array)
-
- instruction.each do |opnd|
- # Only look at arrays. Other operands are literals.
- next unless opnd.is_a?(Array)
-
- # Only look at instruction sequences. Other operands are literals.
- next unless opnd[0] == "YARVInstructionSequence/SimpleDataFormat"
-
- yield ISeq.new(opnd)
- end
- end
- end
- end
-
- private_constant :ISeq
-
- # :call-seq:
- # Debug::cruby_locals(source) -> Array
- #
- # For the given source, compiles with CRuby and returns a list of all of the
- # sets of local variables that were encountered.
- def self.cruby_locals(source)
- verbose, $VERBOSE = $VERBOSE, nil
-
- begin
- locals = [] #: Array[Array[Symbol | Integer]]
- stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)]
-
- while (iseq = stack.pop)
- names = [*iseq.local_table]
- names.map!.with_index do |name, index|
- # When an anonymous local variable is present in the iseq's local
- # table, it is represented as the stack offset from the top.
- # However, when these are dumped to binary and read back in, they
- # are replaced with the symbol :#arg_rest. To consistently handle
- # this, we replace them here with their index.
- if name == :"#arg_rest"
- names.length - index + 1
- else
- name
- end
- end
-
- locals << names
- iseq.each_child { |child| stack << child }
- end
-
- locals
- ensure
- $VERBOSE = verbose
- end
- end
-
- # Used to hold the place of a local that will be in the local table but
- # cannot be accessed directly from the source code. For example, the
- # iteration variable in a for loop or the positional parameter on a method
- # definition that is destructured.
- AnonymousLocal = Object.new
- private_constant :AnonymousLocal
-
- # :call-seq:
- # Debug::prism_locals(source) -> Array
- #
- # For the given source, parses with prism and returns a list of all of the
- # sets of local variables that were encountered.
- def self.prism_locals(source)
- locals = [] #: Array[Array[Symbol | Integer]]
- stack = [Prism.parse(source).value] #: Array[Prism::node]
-
- while (node = stack.pop)
- case node
- when BlockNode, DefNode, LambdaNode
- names = node.locals
- params =
- if node.is_a?(DefNode)
- node.parameters
- elsif node.parameters.is_a?(NumberedParametersNode)
- nil
- else
- node.parameters&.parameters
- end
-
- # prism places parameters in the same order that they appear in the
- # source. CRuby places them in the order that they need to appear
- # according to their own internal calling convention. We mimic that
- # order here so that we can compare properly.
- if params
- sorted = [
- *params.requireds.map do |required|
- if required.is_a?(RequiredParameterNode)
- required.name
- else
- AnonymousLocal
- end
- end,
- *params.optionals.map(&:name),
- *((params.rest.name || :*) if params.rest && !params.rest.is_a?(ImplicitRestNode)),
- *params.posts.map do |post|
- if post.is_a?(RequiredParameterNode)
- post.name
- else
- AnonymousLocal
- end
- end,
- *params.keywords.grep(RequiredKeywordParameterNode).map(&:name),
- *params.keywords.grep(OptionalKeywordParameterNode).map(&:name),
- ]
-
- sorted << AnonymousLocal if params.keywords.any?
-
- if params.keyword_rest.is_a?(ForwardingParameterNode)
- sorted.push(:*, :**, :&, :"...")
- elsif params.keyword_rest.is_a?(KeywordRestParameterNode)
- sorted << (params.keyword_rest.name || :**)
- end
-
- # Recurse down the parameter tree to find any destructured
- # parameters and add them after the other parameters.
- param_stack = params.requireds.concat(params.posts).grep(MultiTargetNode).reverse
- while (param = param_stack.pop)
- case param
- when MultiTargetNode
- param_stack.concat(param.rights.reverse)
- param_stack << param.rest if param.rest&.expression && !sorted.include?(param.rest.expression.name)
- param_stack.concat(param.lefts.reverse)
- when RequiredParameterNode
- sorted << param.name
- when SplatNode
- sorted << param.expression.name
- end
- end
-
- if params.block
- sorted << (params.block.name || :&)
- end
-
- names = sorted.concat(names - sorted)
- end
-
- names.map!.with_index do |name, index|
- if name == AnonymousLocal
- names.length - index + 1
- else
- name
- end
- end
-
- locals << names
- when ClassNode, ModuleNode, ProgramNode, SingletonClassNode
- locals << node.locals
- when ForNode
- locals << [2]
- when PostExecutionNode
- locals.push([], [])
- when InterpolatedRegularExpressionNode
- locals << [] if node.once?
- end
-
- stack.concat(node.compact_child_nodes)
- end
-
- locals
- end
-
- # :call-seq:
- # Debug::newlines(source) -> Array
- #
- # For the given source string, return the byte offsets of every newline in
- # the source.
- def self.newlines(source)
- Prism.parse(source).source.offsets
- end
-
- # A wrapping around prism's internal encoding data structures. This is used
- # for reflection and debugging purposes.
- class Encoding
- # The name of the encoding, that can be passed to Encoding.find.
- attr_reader :name
-
- # Initialize a new encoding with the given name and whether or not it is
- # a multibyte encoding.
- def initialize(name, multibyte)
- @name = name
- @multibyte = multibyte
- end
-
- # Whether or not the encoding is a multibyte encoding.
- def multibyte?
- @multibyte
- end
-
- # Returns the number of bytes of the first character in the source string,
- # if it is valid for the encoding. Otherwise, returns 0.
- def width(source)
- Encoding._width(name, source)
- end
-
- # Returns true if the first character in the source string is a valid
- # alphanumeric character for the encoding.
- def alnum?(source)
- Encoding._alnum?(name, source)
- end
-
- # Returns true if the first character in the source string is a valid
- # alphabetic character for the encoding.
- def alpha?(source)
- Encoding._alpha?(name, source)
- end
-
- # Returns true if the first character in the source string is a valid
- # uppercase character for the encoding.
- def upper?(source)
- Encoding._upper?(name, source)
- end
- end
- end
-end
diff --git a/lib/prism/desugar_compiler.rb b/lib/prism/desugar_compiler.rb
index de02445149..e3b15fc3b0 100644
--- a/lib/prism/desugar_compiler.rb
+++ b/lib/prism/desugar_compiler.rb
@@ -2,11 +2,13 @@
module Prism
class DesugarAndWriteNode # :nodoc:
- attr_reader :node, :source, :read_class, :write_class, :arguments
+ include DSL
- def initialize(node, source, read_class, write_class, *arguments)
+ attr_reader :node, :default_source, :read_class, :write_class, :arguments
+
+ def initialize(node, default_source, read_class, write_class, **arguments)
@node = node
- @source = source
+ @default_source = default_source
@read_class = read_class
@write_class = write_class
@arguments = arguments
@@ -14,22 +16,30 @@ module Prism
# Desugar `x &&= y` to `x && x = y`
def compile
- AndNode.new(
- source,
- read_class.new(source, *arguments, node.name_loc),
- write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location),
- node.operator_loc,
- node.location
+ and_node(
+ location: node.location,
+ left: public_send(read_class, location: node.name_loc, **arguments),
+ right: public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ ),
+ operator_loc: node.operator_loc
)
end
end
class DesugarOrWriteDefinedNode # :nodoc:
- attr_reader :node, :source, :read_class, :write_class, :arguments
+ include DSL
+
+ attr_reader :node, :default_source, :read_class, :write_class, :arguments
- def initialize(node, source, read_class, write_class, *arguments)
+ def initialize(node, default_source, read_class, write_class, **arguments)
@node = node
- @source = source
+ @default_source = default_source
@read_class = read_class
@write_class = write_class
@arguments = arguments
@@ -37,35 +47,50 @@ module Prism
# Desugar `x ||= y` to `defined?(x) ? x : x = y`
def compile
- IfNode.new(
- source,
- node.operator_loc,
- DefinedNode.new(source, nil, read_class.new(source, *arguments, node.name_loc), nil, node.operator_loc, node.name_loc),
- node.operator_loc,
- StatementsNode.new(source, [read_class.new(source, *arguments, node.name_loc)], node.location),
- ElseNode.new(
- source,
- node.operator_loc,
- StatementsNode.new(
- source,
- [write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location)],
- node.location
+ if_node(
+ location: node.location,
+ if_keyword_loc: node.operator_loc,
+ predicate: defined_node(
+ location: node.name_loc,
+ value: public_send(read_class, location: node.name_loc, **arguments),
+ keyword_loc: node.operator_loc
+ ),
+ then_keyword_loc: node.operator_loc,
+ statements: statements_node(
+ location: node.location,
+ body: [public_send(read_class, location: node.name_loc, **arguments)]
+ ),
+ subsequent: else_node(
+ location: node.location,
+ else_keyword_loc: node.operator_loc,
+ statements: statements_node(
+ location: node.location,
+ body: [
+ public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ )
+ ]
),
- node.operator_loc,
- node.location
+ end_keyword_loc: node.operator_loc
),
- node.operator_loc,
- node.location
+ end_keyword_loc: node.operator_loc
)
end
end
class DesugarOperatorWriteNode # :nodoc:
- attr_reader :node, :source, :read_class, :write_class, :arguments
+ include DSL
- def initialize(node, source, read_class, write_class, *arguments)
+ attr_reader :node, :default_source, :read_class, :write_class, :arguments
+
+ def initialize(node, default_source, read_class, write_class, **arguments)
@node = node
- @source = source
+ @default_source = default_source
@read_class = read_class
@write_class = write_class
@arguments = arguments
@@ -75,35 +100,41 @@ module Prism
def compile
binary_operator_loc = node.binary_operator_loc.chop
- write_class.new(
- source,
- *arguments,
- node.name_loc,
- CallNode.new(
- source,
- 0,
- read_class.new(source, *arguments, node.name_loc),
- nil,
- binary_operator_loc.slice.to_sym,
- binary_operator_loc,
- nil,
- ArgumentsNode.new(source, 0, [node.value], node.value.location),
- nil,
- nil,
- node.location
+ public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: call_node(
+ location: node.location,
+ receiver: public_send(
+ read_class,
+ location: node.name_loc,
+ **arguments
+ ),
+ name: binary_operator_loc.slice.to_sym,
+ message_loc: binary_operator_loc,
+ arguments: arguments_node(
+ location: node.value.location,
+ arguments: [node.value]
+ )
),
- node.binary_operator_loc.copy(start_offset: node.binary_operator_loc.end_offset - 1, length: 1),
- node.location
+ operator_loc: node.binary_operator_loc.copy(
+ start_offset: node.binary_operator_loc.end_offset - 1,
+ length: 1
+ )
)
end
end
class DesugarOrWriteNode # :nodoc:
- attr_reader :node, :source, :read_class, :write_class, :arguments
+ include DSL
- def initialize(node, source, read_class, write_class, *arguments)
+ attr_reader :node, :default_source, :read_class, :write_class, :arguments
+
+ def initialize(node, default_source, read_class, write_class, **arguments)
@node = node
- @source = source
+ @default_source = default_source
@read_class = read_class
@write_class = write_class
@arguments = arguments
@@ -111,12 +142,18 @@ module Prism
# Desugar `x ||= y` to `x || x = y`
def compile
- OrNode.new(
- source,
- read_class.new(source, *arguments, node.name_loc),
- write_class.new(source, *arguments, node.name_loc, node.value, node.operator_loc, node.location),
- node.operator_loc,
- node.location
+ or_node(
+ location: node.location,
+ left: public_send(read_class, location: node.name_loc, **arguments),
+ right: public_send(
+ write_class,
+ location: node.location,
+ **arguments,
+ name_loc: node.name_loc,
+ value: node.value,
+ operator_loc: node.operator_loc
+ ),
+ operator_loc: node.operator_loc
)
end
end
@@ -125,91 +162,91 @@ module Prism
class ClassVariableAndWriteNode
def desugar # :nodoc:
- DesugarAndWriteNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile
+ DesugarAndWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
end
end
class ClassVariableOrWriteNode
def desugar # :nodoc:
- DesugarOrWriteDefinedNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile
+ DesugarOrWriteDefinedNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
end
end
class ClassVariableOperatorWriteNode
def desugar # :nodoc:
- DesugarOperatorWriteNode.new(self, source, ClassVariableReadNode, ClassVariableWriteNode, name).compile
+ DesugarOperatorWriteNode.new(self, source, :class_variable_read_node, :class_variable_write_node, name: name).compile
end
end
class ConstantAndWriteNode
def desugar # :nodoc:
- DesugarAndWriteNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile
+ DesugarAndWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
end
end
class ConstantOrWriteNode
def desugar # :nodoc:
- DesugarOrWriteDefinedNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile
+ DesugarOrWriteDefinedNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
end
end
class ConstantOperatorWriteNode
def desugar # :nodoc:
- DesugarOperatorWriteNode.new(self, source, ConstantReadNode, ConstantWriteNode, name).compile
+ DesugarOperatorWriteNode.new(self, source, :constant_read_node, :constant_write_node, name: name).compile
end
end
class GlobalVariableAndWriteNode
def desugar # :nodoc:
- DesugarAndWriteNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile
+ DesugarAndWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
end
end
class GlobalVariableOrWriteNode
def desugar # :nodoc:
- DesugarOrWriteDefinedNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile
+ DesugarOrWriteDefinedNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
end
end
class GlobalVariableOperatorWriteNode
def desugar # :nodoc:
- DesugarOperatorWriteNode.new(self, source, GlobalVariableReadNode, GlobalVariableWriteNode, name).compile
+ DesugarOperatorWriteNode.new(self, source, :global_variable_read_node, :global_variable_write_node, name: name).compile
end
end
class InstanceVariableAndWriteNode
def desugar # :nodoc:
- DesugarAndWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile
+ DesugarAndWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
end
end
class InstanceVariableOrWriteNode
def desugar # :nodoc:
- DesugarOrWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile
+ DesugarOrWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
end
end
class InstanceVariableOperatorWriteNode
def desugar # :nodoc:
- DesugarOperatorWriteNode.new(self, source, InstanceVariableReadNode, InstanceVariableWriteNode, name).compile
+ DesugarOperatorWriteNode.new(self, source, :instance_variable_read_node, :instance_variable_write_node, name: name).compile
end
end
class LocalVariableAndWriteNode
def desugar # :nodoc:
- DesugarAndWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile
+ DesugarAndWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
end
end
class LocalVariableOrWriteNode
def desugar # :nodoc:
- DesugarOrWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile
+ DesugarOrWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
end
end
class LocalVariableOperatorWriteNode
def desugar # :nodoc:
- DesugarOperatorWriteNode.new(self, source, LocalVariableReadNode, LocalVariableWriteNode, name, depth).compile
+ DesugarOperatorWriteNode.new(self, source, :local_variable_read_node, :local_variable_write_node, name: name, depth: depth).compile
end
end
diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb
index b62a59d037..95206680f9 100644
--- a/lib/prism/ffi.rb
+++ b/lib/prism/ffi.rb
@@ -72,6 +72,7 @@ module Prism
end
callback :pm_parse_stream_fgets_t, [:pointer, :int, :pointer], :pointer
+ enum :pm_string_init_result_t, %i[PM_STRING_INIT_SUCCESS PM_STRING_INIT_ERROR_GENERIC PM_STRING_INIT_ERROR_DIRECTORY]
load_exported_functions_from(
"prism.h",
@@ -176,13 +177,26 @@ module Prism
def self.with_file(filepath)
raise TypeError unless filepath.is_a?(String)
+ # On Windows and Mac, it's expected that filepaths will be encoded in
+ # UTF-8. If they are not, we need to convert them to UTF-8 before
+ # passing them into pm_string_mapped_init.
+ if RbConfig::CONFIG["host_os"].match?(/bccwin|cygwin|djgpp|mingw|mswin|wince|darwin/i) &&
+ (encoding = filepath.encoding) != Encoding::ASCII_8BIT && encoding != Encoding::UTF_8
+ filepath = filepath.encode(Encoding::UTF_8)
+ end
+
FFI::MemoryPointer.new(SIZEOF) do |pm_string|
- if LibRubyParser.pm_string_mapped_init(pm_string, filepath)
+ case (result = LibRubyParser.pm_string_mapped_init(pm_string, filepath))
+ when :PM_STRING_INIT_SUCCESS
pointer = LibRubyParser.pm_string_source(pm_string)
length = LibRubyParser.pm_string_length(pm_string)
return yield new(pointer, length, false)
- else
+ when :PM_STRING_INIT_ERROR_GENERIC
raise SystemCallError.new(filepath, FFI.errno)
+ when :PM_STRING_INIT_ERROR_DIRECTORY
+ raise Errno::EISDIR.new(filepath)
+ else
+ raise "Unknown error initializing pm_string_t: #{result.inspect}"
end
ensure
LibRubyParser.pm_string_free(pm_string)
@@ -200,8 +214,8 @@ module Prism
class << self
# Mirror the Prism.dump API by using the serialization API.
- def dump(code, **options)
- LibRubyParser::PrismString.with_string(code) { |string| dump_common(string, options) }
+ def dump(source, **options)
+ LibRubyParser::PrismString.with_string(source) { |string| dump_common(string, options) }
end
# Mirror the Prism.dump_file API by using the serialization API.
@@ -302,6 +316,27 @@ module Prism
!parse_file_success?(filepath, **options)
end
+ # Mirror the Prism.profile API by using the serialization API.
+ def profile(source, **options)
+ LibRubyParser::PrismString.with_string(source) do |string|
+ LibRubyParser::PrismBuffer.with do |buffer|
+ LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
+ nil
+ end
+ end
+ end
+
+ # Mirror the Prism.profile_file API by using the serialization API.
+ def profile_file(filepath, **options)
+ LibRubyParser::PrismString.with_file(filepath) do |string|
+ LibRubyParser::PrismBuffer.with do |buffer|
+ options[:filepath] = filepath
+ LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
+ nil
+ end
+ end
+ end
+
private
def dump_common(string, options) # :nodoc:
@@ -394,7 +429,7 @@ module Prism
template << "L"
if (encoding = options[:encoding])
- name = encoding.name
+ name = encoding.is_a?(Encoding) ? encoding.name : encoding
values.push(name.bytesize, name.b)
template << "A*"
else
@@ -410,6 +445,15 @@ module Prism
template << "C"
values << { nil => 0, "3.3.0" => 1, "3.3.1" => 1, "3.4.0" => 0, "latest" => 0 }.fetch(options[:version])
+ template << "C"
+ values << (options[:encoding] == false ? 1 : 0)
+
+ template << "C"
+ values << (options.fetch(:main_script, false) ? 1 : 0)
+
+ template << "C"
+ values << (options.fetch(:partial_script, false) ? 1 : 0)
+
template << "L"
if (scopes = options[:scopes])
values << scopes.length
diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb
index ceec76b8d6..4dfcebd638 100644
--- a/lib/prism/node_ext.rb
+++ b/lib/prism/node_ext.rb
@@ -5,10 +5,13 @@
module Prism
class Node
def deprecated(*replacements) # :nodoc:
+ location = caller_locations(1, 1)
+ location = location[0].label if location
suggest = replacements.map { |replacement| "#{self.class}##{replacement}" }
+
warn(<<~MSG, category: :deprecated)
- [deprecation]: #{self.class}##{caller_locations(1, 1)[0].label} is deprecated \
- and will be removed in the next major version. Use #{suggest.join("/")} instead.
+ [deprecation]: #{self.class}##{location} is deprecated and will be \
+ removed in the next major version. Use #{suggest.join("/")} instead.
#{(caller(1, 3) || []).join("\n")}
MSG
end
@@ -18,7 +21,10 @@ module Prism
# Returns a numeric value that represents the flags that were used to create
# the regular expression.
def options
- o = flags & (RegularExpressionFlags::IGNORE_CASE | RegularExpressionFlags::EXTENDED | RegularExpressionFlags::MULTI_LINE)
+ o = 0
+ o |= Regexp::IGNORECASE if flags.anybits?(RegularExpressionFlags::IGNORE_CASE)
+ o |= Regexp::EXTENDED if flags.anybits?(RegularExpressionFlags::EXTENDED)
+ o |= Regexp::MULTILINE if flags.anybits?(RegularExpressionFlags::MULTI_LINE)
o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8)
o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT)
o
@@ -66,11 +72,12 @@ module Prism
def to_interpolated
InterpolatedStringNode.new(
source,
+ -1,
+ location,
frozen? ? InterpolatedStringNodeFlags::FROZEN : 0,
opening_loc,
- [copy(opening_loc: nil, closing_loc: nil, location: content_loc)],
- closing_loc,
- location
+ [copy(location: content_loc, opening_loc: nil, closing_loc: nil)],
+ closing_loc
)
end
end
@@ -83,10 +90,12 @@ module Prism
def to_interpolated
InterpolatedXStringNode.new(
source,
+ -1,
+ location,
+ flags,
opening_loc,
- [StringNode.new(source, 0, nil, content_loc, nil, unescaped, content_loc)],
- closing_loc,
- location
+ [StringNode.new(source, node_id, content_loc, 0, nil, content_loc, nil, unescaped)],
+ closing_loc
)
end
end
@@ -103,7 +112,19 @@ module Prism
class RationalNode < Node
# Returns the value of the node as a Ruby Rational.
def value
- Rational(numeric.is_a?(IntegerNode) ? numeric.value : slice.chomp("r"))
+ Rational(numerator, denominator)
+ end
+
+ # Returns the value of the node as an IntegerNode or a FloatNode. This
+ # method is deprecated in favor of #value or #numerator/#denominator.
+ def numeric
+ deprecated("value", "numerator", "denominator")
+
+ if denominator == 1
+ IntegerNode.new(source, -1, location.chop, flags, numerator)
+ else
+ FloatNode.new(source, -1, location.chop, 0, numerator.to_f / denominator)
+ end
end
end
@@ -180,7 +201,12 @@ module Prism
# continue to supply that API.
def child
deprecated("name", "name_loc")
- name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location)
+
+ if name
+ ConstantReadNode.new(source, -1, name_loc, 0, name)
+ else
+ MissingNode.new(source, -1, location, 0)
+ end
end
end
@@ -216,7 +242,12 @@ module Prism
# continue to supply that API.
def child
deprecated("name", "name_loc")
- name ? ConstantReadNode.new(source, name, name_loc) : MissingNode.new(source, location)
+
+ if name
+ ConstantReadNode.new(source, -1, name_loc, 0, name)
+ else
+ MissingNode.new(source, -1, location, 0)
+ end
end
end
@@ -249,9 +280,10 @@ module Prism
end
posts.each do |param|
- if param.is_a?(MultiTargetNode)
+ case param
+ when MultiTargetNode
names << [:req]
- elsif param.is_a?(NoKeywordsParameterNode)
+ when NoKeywordsParameterNode, KeywordRestParameterNode, ForwardingParameterNode
# Invalid syntax, e.g. "def f(**nil, ...)" moves the NoKeywordsParameterNode to posts
raise "Invalid syntax"
else
@@ -428,4 +460,49 @@ module Prism
binary_operator_loc
end
end
+
+ class CaseMatchNode < Node
+ # Returns the else clause of the case match node. This method is deprecated
+ # in favor of #else_clause.
+ def consequent
+ deprecated("else_clause")
+ else_clause
+ end
+ end
+
+ class CaseNode < Node
+ # Returns the else clause of the case node. This method is deprecated in
+ # favor of #else_clause.
+ def consequent
+ deprecated("else_clause")
+ else_clause
+ end
+ end
+
+ class IfNode < Node
+ # Returns the subsequent if/elsif/else clause of the if node. This method is
+ # deprecated in favor of #subsequent.
+ def consequent
+ deprecated("subsequent")
+ subsequent
+ end
+ end
+
+ class RescueNode < Node
+ # Returns the subsequent rescue clause of the rescue node. This method is
+ # deprecated in favor of #subsequent.
+ def consequent
+ deprecated("subsequent")
+ subsequent
+ end
+ end
+
+ class UnlessNode < Node
+ # Returns the else clause of the unless node. This method is deprecated in
+ # favor of #else_clause.
+ def consequent
+ deprecated("else_clause")
+ else_clause
+ end
+ end
end
diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb
index 798fde09e5..ae026b42ac 100644
--- a/lib/prism/parse_result.rb
+++ b/lib/prism/parse_result.rb
@@ -10,7 +10,11 @@ module Prism
# specialized and more performant `ASCIISource` if no multibyte characters
# are present in the source code.
def self.for(source, start_line = 1, offsets = [])
- source.ascii_only? ? ASCIISource.new(source, start_line, offsets): new(source, start_line, offsets)
+ if source.ascii_only?
+ ASCIISource.new(source, start_line, offsets)
+ else
+ new(source, start_line, offsets)
+ end
end
# The source code that this source object represents.
@@ -87,7 +91,12 @@ module Prism
# encodings, it is not captured here.
def code_units_offset(byte_offset, encoding)
byteslice = (source.byteslice(0, byte_offset) or raise).encode(encoding)
- (encoding == Encoding::UTF_16LE || encoding == Encoding::UTF_16BE) ? (byteslice.bytesize / 2) : byteslice.length
+
+ if encoding == Encoding::UTF_16LE || encoding == Encoding::UTF_16BE
+ byteslice.bytesize / 2
+ else
+ byteslice.length
+ end
end
# Returns the column number in code units for the given encoding for the
@@ -146,7 +155,7 @@ module Prism
# Specialized version of `code_units_column` that does not depend on
# `code_units_offset`, which is a more expensive operation. This is
- # essentialy the same as `Prism::Source#column`.
+ # essentially the same as `Prism::Source#column`.
def code_units_column(byte_offset, encoding)
byte_offset - line_start(byte_offset)
end
@@ -575,9 +584,11 @@ module Prism
# This is a result specific to the `parse` and `parse_file` methods.
class ParseResult < Result
autoload :Comments, "prism/parse_result/comments"
+ autoload :Errors, "prism/parse_result/errors"
autoload :Newlines, "prism/parse_result/newlines"
private_constant :Comments
+ private_constant :Errors
private_constant :Newlines
# The syntax tree that was parsed from the source code.
@@ -604,6 +615,12 @@ module Prism
def mark_newlines!
value.accept(Newlines.new(source.offsets.size)) # steep:ignore
end
+
+ # Returns a string representation of the syntax tree with the errors
+ # displayed inline.
+ def errors_format
+ Errors.new(self).format
+ end
end
# This is a result specific to the `lex` and `lex_file` methods.
@@ -694,5 +711,11 @@ module Prism
other.type == type &&
other.value == value
end
+
+ # Returns a string representation of this token.
+ def inspect
+ location
+ super
+ end
end
end
diff --git a/lib/prism/parse_result/errors.rb b/lib/prism/parse_result/errors.rb
new file mode 100644
index 0000000000..847a8442fe
--- /dev/null
+++ b/lib/prism/parse_result/errors.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require "stringio"
+
+module Prism
+ class ParseResult < Result
+ # An object to represent the set of errors on a parse result. This object
+ # can be used to format the errors in a human-readable way.
+ class Errors
+ # The parse result that contains the errors.
+ attr_reader :parse_result
+
+ # Initialize a new set of errors from the given parse result.
+ def initialize(parse_result)
+ @parse_result = parse_result
+ end
+
+ # Formats the errors in a human-readable way and return them as a string.
+ def format
+ error_lines = {}
+ parse_result.errors.each do |error|
+ location = error.location
+ (location.start_line..location.end_line).each do |line|
+ error_lines[line] ||= []
+ error_lines[line] << error
+ end
+ end
+
+ source_lines = parse_result.source.source.lines
+ source_lines << "" if error_lines.key?(source_lines.size + 1)
+
+ io = StringIO.new
+ source_lines.each.with_index(1) do |line, line_number|
+ io.puts(line)
+
+ (error_lines.delete(line_number) || []).each do |error|
+ location = error.location
+
+ case line_number
+ when location.start_line
+ io.print(" " * location.start_column + "^")
+
+ if location.start_line == location.end_line
+ if location.start_column != location.end_column
+ io.print("~" * (location.end_column - location.start_column - 1))
+ end
+
+ io.puts(" " + error.message)
+ else
+ io.puts("~" * (line.bytesize - location.start_column))
+ end
+ when location.end_line
+ io.puts("~" * location.end_column + " " + error.message)
+ else
+ io.puts("~" * line.bytesize)
+ end
+ end
+ end
+
+ io.puts
+ io.string
+ end
+ end
+ end
+end
diff --git a/lib/prism/parse_result/newlines.rb b/lib/prism/parse_result/newlines.rb
index cc1343dfda..a04fa78a75 100644
--- a/lib/prism/parse_result/newlines.rb
+++ b/lib/prism/parse_result/newlines.rb
@@ -25,6 +25,7 @@ module Prism
class Newlines < Visitor
# Create a new Newlines visitor with the given newline offsets.
def initialize(lines)
+ # @type var lines: Integer
@lines = Array.new(1 + lines, false)
end
@@ -44,7 +45,7 @@ module Prism
# Mark if/unless nodes as newlines.
def visit_if_node(node)
- node.newline!(@lines)
+ node.newline_flag!(@lines)
super(node)
end
@@ -53,7 +54,7 @@ module Prism
# Permit statements lists to mark newlines within themselves.
def visit_statements_node(node)
node.body.each do |child|
- child.newline!(@lines)
+ child.newline_flag!(@lines)
end
super(node)
end
@@ -61,93 +62,93 @@ module Prism
end
class Node
- def newline? # :nodoc:
- @newline ? true : false
+ def newline_flag? # :nodoc:
+ @newline_flag ? true : false
end
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
line = location.start_line
unless lines[line]
lines[line] = true
- @newline = true
+ @newline_flag = true
end
end
end
class BeginNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
# Never mark BeginNode with a newline flag, mark children instead.
end
end
class ParenthesesNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
# Never mark ParenthesesNode with a newline flag, mark children instead.
end
end
class IfNode < Node
- def newline!(lines) # :nodoc:
- predicate.newline!(lines)
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
end
end
class UnlessNode < Node
- def newline!(lines) # :nodoc:
- predicate.newline!(lines)
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
end
end
class UntilNode < Node
- def newline!(lines) # :nodoc:
- predicate.newline!(lines)
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
end
end
class WhileNode < Node
- def newline!(lines) # :nodoc:
- predicate.newline!(lines)
+ def newline_flag!(lines) # :nodoc:
+ predicate.newline_flag!(lines)
end
end
class RescueModifierNode < Node
- def newline!(lines) # :nodoc:
- expression.newline!(lines)
+ def newline_flag!(lines) # :nodoc:
+ expression.newline_flag!(lines)
end
end
class InterpolatedMatchLastLineNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
first = parts.first
- first.newline!(lines) if first
+ first.newline_flag!(lines) if first
end
end
class InterpolatedRegularExpressionNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
first = parts.first
- first.newline!(lines) if first
+ first.newline_flag!(lines) if first
end
end
class InterpolatedStringNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
first = parts.first
- first.newline!(lines) if first
+ first.newline_flag!(lines) if first
end
end
class InterpolatedSymbolNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
first = parts.first
- first.newline!(lines) if first
+ first.newline_flag!(lines) if first
end
end
class InterpolatedXStringNode < Node
- def newline!(lines) # :nodoc:
+ def newline_flag!(lines) # :nodoc:
first = parts.first
- first.newline!(lines) if first
+ first.newline_flag!(lines) if first
end
end
end
diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec
index 374591bb70..37aa979576 100644
--- a/lib/prism/prism.gemspec
+++ b/lib/prism/prism.gemspec
@@ -2,7 +2,7 @@
Gem::Specification.new do |spec|
spec.name = "prism"
- spec.version = "0.29.0"
+ spec.version = "1.0.0"
spec.authors = ["Shopify"]
spec.email = ["ruby@shopify.com"]
@@ -65,12 +65,10 @@ Gem::Specification.new do |spec|
"include/prism/util/pm_newline_list.h",
"include/prism/util/pm_strncasecmp.h",
"include/prism/util/pm_string.h",
- "include/prism/util/pm_string_list.h",
"include/prism/util/pm_strpbrk.h",
"include/prism/version.h",
"lib/prism.rb",
"lib/prism/compiler.rb",
- "lib/prism/debug.rb",
"lib/prism/desugar_compiler.rb",
"lib/prism/dispatcher.rb",
"lib/prism/dot_visitor.rb",
@@ -84,6 +82,7 @@ Gem::Specification.new do |spec|
"lib/prism/pack.rb",
"lib/prism/parse_result.rb",
"lib/prism/parse_result/comments.rb",
+ "lib/prism/parse_result/errors.rb",
"lib/prism/parse_result/newlines.rb",
"lib/prism/pattern.rb",
"lib/prism/polyfill/byteindex.rb",
@@ -96,7 +95,6 @@ Gem::Specification.new do |spec|
"lib/prism/translation/parser34.rb",
"lib/prism/translation/parser/compiler.rb",
"lib/prism/translation/parser/lexer.rb",
- "lib/prism/translation/parser/rubocop.rb",
"lib/prism/translation/ripper.rb",
"lib/prism/translation/ripper/sexp.rb",
"lib/prism/translation/ripper/shim.rb",
@@ -105,6 +103,7 @@ Gem::Specification.new do |spec|
"prism.gemspec",
"rbi/prism.rbi",
"rbi/prism/compiler.rbi",
+ "rbi/prism/dsl.rbi",
"rbi/prism/inspect_visitor.rbi",
"rbi/prism/node_ext.rbi",
"rbi/prism/node.rbi",
@@ -149,7 +148,6 @@ Gem::Specification.new do |spec|
"src/util/pm_list.c",
"src/util/pm_memchr.c",
"src/util/pm_newline_list.c",
- "src/util/pm_string_list.c",
"src/util/pm_string.c",
"src/util/pm_strncasecmp.c",
"src/util/pm_strpbrk.c"
diff --git a/lib/prism/translation/parser.rb b/lib/prism/translation/parser.rb
index 3748fc896e..8c7eb3aa75 100644
--- a/lib/prism/translation/parser.rb
+++ b/lib/prism/translation/parser.rb
@@ -51,7 +51,7 @@ module Prism
source = source_buffer.source
offset_cache = build_offset_cache(source)
- result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache)
+ result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache)
build_ast(result.value, offset_cache)
ensure
@@ -64,7 +64,7 @@ module Prism
source = source_buffer.source
offset_cache = build_offset_cache(source)
- result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache)
+ result = unwrap(Prism.parse(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache)
[
build_ast(result.value, offset_cache),
@@ -83,7 +83,7 @@ module Prism
offset_cache = build_offset_cache(source)
result =
begin
- unwrap(Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]]), offset_cache)
+ unwrap(Prism.parse_lex(source, filepath: source_buffer.name, version: convert_for_prism(version), scopes: [[]], encoding: false), offset_cache)
rescue ::Parser::SyntaxError
raise if !recover
end
diff --git a/lib/prism/translation/parser/compiler.rb b/lib/prism/translation/parser/compiler.rb
index a6c3118efd..d57b5757d7 100644
--- a/lib/prism/translation/parser/compiler.rb
+++ b/lib/prism/translation/parser/compiler.rb
@@ -90,7 +90,11 @@ module Prism
end
if node.constant
- builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc))
+ if visited.empty?
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc))
+ else
+ builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc))
+ end
else
builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc))
end
@@ -105,38 +109,46 @@ module Prism
# { a: 1 }
# ^^^^
def visit_assoc_node(node)
+ key = node.key
+
if in_pattern
if node.value.is_a?(ImplicitNode)
- if node.key.is_a?(SymbolNode)
- builder.match_hash_var([node.key.unescaped, srange(node.key.location)])
+ if key.is_a?(SymbolNode)
+ if key.opening.nil?
+ builder.match_hash_var([key.unescaped, srange(key.location)])
+ else
+ builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc))
+ end
else
- builder.match_hash_var_from_str(token(node.key.opening_loc), visit_all(node.key.parts), token(node.key.closing_loc))
+ builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc))
end
+ elsif key.opening.nil?
+ builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value))
else
- builder.pair_keyword([node.key.unescaped, srange(node.key.location)], visit(node.value))
+ builder.pair_quoted(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc), visit(node.value))
end
elsif node.value.is_a?(ImplicitNode)
if (value = node.value.value).is_a?(LocalVariableReadNode)
builder.pair_keyword(
- [node.key.unescaped, srange(node.key)],
- builder.ident([value.name, srange(node.key.value_loc)]).updated(:lvar)
+ [key.unescaped, srange(key)],
+ builder.ident([value.name, srange(key.value_loc)]).updated(:lvar)
)
else
- builder.pair_label([node.key.unescaped, srange(node.key.location)])
+ builder.pair_label([key.unescaped, srange(key.location)])
end
elsif node.operator_loc
- builder.pair(visit(node.key), token(node.operator_loc), visit(node.value))
- elsif node.key.is_a?(SymbolNode) && node.key.opening_loc.nil?
- builder.pair_keyword([node.key.unescaped, srange(node.key.location)], visit(node.value))
+ builder.pair(visit(key), token(node.operator_loc), visit(node.value))
+ elsif key.is_a?(SymbolNode) && key.opening_loc.nil?
+ builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value))
else
parts =
- if node.key.is_a?(SymbolNode)
- [builder.string_internal([node.key.unescaped, srange(node.key.value_loc)])]
+ if key.is_a?(SymbolNode)
+ [builder.string_internal([key.unescaped, srange(key.value_loc)])]
else
- visit_all(node.key.parts)
+ visit_all(key.parts)
end
- builder.pair_quoted(token(node.key.opening_loc), parts, token(node.key.closing_loc), visit(node.value))
+ builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value))
end
end
@@ -146,7 +158,9 @@ module Prism
# { **foo }
# ^^^^^
def visit_assoc_splat_node(node)
- if node.value.nil? && forwarding.include?(:**)
+ if in_pattern
+ builder.match_rest(token(node.operator_loc), token(node.value&.location))
+ elsif node.value.nil? && forwarding.include?(:**)
builder.forwarded_kwrestarg(token(node.operator_loc))
else
builder.kwsplat(token(node.operator_loc), visit(node.value))
@@ -167,7 +181,7 @@ module Prism
if (rescue_clause = node.rescue_clause)
begin
find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset
- find_end_offset = (rescue_clause.statements&.location&.start_offset || rescue_clause.consequent&.location&.start_offset || (find_start_offset + 1))
+ find_end_offset = (rescue_clause.statements&.location&.start_offset || rescue_clause.subsequent&.location&.start_offset || (find_start_offset + 1))
rescue_bodies << builder.rescue_body(
token(rescue_clause.keyword_loc),
@@ -177,7 +191,7 @@ module Prism
srange_find(find_start_offset, find_end_offset, [";"]),
visit(rescue_clause.statements)
)
- end until (rescue_clause = rescue_clause.consequent).nil?
+ end until (rescue_clause = rescue_clause.subsequent).nil?
end
begin_body =
@@ -396,8 +410,8 @@ module Prism
token(node.case_keyword_loc),
visit(node.predicate),
visit_all(node.conditions),
- token(node.consequent&.else_keyword_loc),
- visit(node.consequent),
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.else_clause),
token(node.end_keyword_loc)
)
end
@@ -409,8 +423,8 @@ module Prism
token(node.case_keyword_loc),
visit(node.predicate),
visit_all(node.conditions),
- token(node.consequent&.else_keyword_loc),
- visit(node.consequent),
+ token(node.else_clause&.else_keyword_loc),
+ visit(node.else_clause),
token(node.end_keyword_loc)
)
end
@@ -844,8 +858,8 @@ module Prism
visit(node.predicate),
token(node.then_keyword_loc),
visit(node.statements),
- token(node.consequent.else_keyword_loc),
- visit(node.consequent)
+ token(node.subsequent.else_keyword_loc),
+ visit(node.subsequent)
)
elsif node.if_keyword_loc.start_offset == node.location.start_offset
builder.condition(
@@ -854,16 +868,16 @@ module Prism
if node.then_keyword_loc
token(node.then_keyword_loc)
else
- srange_find(node.predicate.location.end_offset, (node.statements&.location || node.consequent&.location || node.end_keyword_loc).start_offset, [";"])
+ srange_find(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset, [";"])
end,
visit(node.statements),
- case node.consequent
+ case node.subsequent
when IfNode
- token(node.consequent.if_keyword_loc)
+ token(node.subsequent.if_keyword_loc)
when ElseNode
- token(node.consequent.else_keyword_loc)
+ token(node.subsequent.else_keyword_loc)
end,
- visit(node.consequent),
+ visit(node.subsequent),
if node.if_keyword != "elsif"
token(node.end_keyword_loc)
end
@@ -871,7 +885,7 @@ module Prism
else
builder.condition_mod(
visit(node.statements),
- visit(node.consequent),
+ visit(node.subsequent),
token(node.if_keyword_loc),
visit(node.predicate)
)
@@ -881,7 +895,7 @@ module Prism
# 1i
# ^^
def visit_imaginary_node(node)
- visit_numeric(node, builder.complex([imaginary_value(node), srange(node.location)]))
+ visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)]))
end
# { foo: }
@@ -1064,36 +1078,7 @@ module Prism
# ^^^^^^^^^^^^
def visit_interpolated_string_node(node)
if node.heredoc?
- children, closing = visit_heredoc(node)
- opening = token(node.opening_loc)
-
- start_offset = node.opening_loc.end_offset + 1
- end_offset = node.parts.first.location.start_offset
-
- # In the below case, the offsets should be the same:
- #
- # <<~HEREDOC
- # a #{b}
- # HEREDOC
- #
- # But in this case, the end_offset would be greater than the start_offset:
- #
- # <<~HEREDOC
- # #{b}
- # HEREDOC
- #
- # So we need to make sure the result node's heredoc range is correct, without updating the children
- result = if start_offset < end_offset
- # We need to add a padding string to ensure that the heredoc has correct range for its body
- padding_string_node = builder.string_internal(["", srange_offsets(start_offset, end_offset)])
- node_with_correct_location = builder.string_compose(opening, [padding_string_node, *children], closing)
- # But the padding string should not be included in the final AST, so we need to update the result's children
- node_with_correct_location.updated(:dstr, children)
- else
- builder.string_compose(opening, children, closing)
- end
-
- return result
+ return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
end
parts = if node.parts.one? { |part| part.type == :string_node }
@@ -1137,8 +1122,7 @@ module Prism
# ^^^^^^^^^^^^
def visit_interpolated_x_string_node(node)
if node.heredoc?
- children, closing = visit_heredoc(node)
- builder.xstring_compose(token(node.opening_loc), children, closing)
+ visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
else
builder.xstring_compose(
token(node.opening_loc),
@@ -1149,6 +1133,12 @@ module Prism
end
# -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ builder.ident([:it, srange(node.location)]).updated(:lvar)
+ end
+
+ # -> { it }
# ^^^^^^^^^
def visit_it_parameters_node(node)
builder.args(nil, [], nil, false)
@@ -1201,14 +1191,7 @@ module Prism
# foo
# ^^^
def visit_local_variable_read_node(node)
- name = node.name
-
- # This is just a guess. parser doesn't have support for the implicit
- # `it` variable yet, so we'll probably have to visit this once it
- # does.
- name = :it if name == :"0it"
-
- builder.ident([name, srange(node.location)]).updated(:lvar)
+ builder.ident([node.name, srange(node.location)]).updated(:lvar)
end
# foo = 1
@@ -1312,13 +1295,9 @@ module Prism
# foo, bar = baz
# ^^^^^^^^
def visit_multi_target_node(node)
- elements = [*node.lefts]
- elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
- elements.concat(node.rights)
-
builder.multi_lhs(
token(node.lparen_loc),
- visit_all(elements),
+ visit_all(multi_target_elements(node)),
token(node.rparen_loc)
)
end
@@ -1326,9 +1305,11 @@ module Prism
# foo, bar = baz
# ^^^^^^^^^^^^^^
def visit_multi_write_node(node)
- elements = [*node.lefts]
- elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
- elements.concat(node.rights)
+ elements = multi_target_elements(node)
+
+ if elements.length == 1 && elements.first.is_a?(MultiTargetNode)
+ elements = multi_target_elements(elements.first)
+ end
builder.multi_assign(
builder.multi_lhs(
@@ -1409,12 +1390,12 @@ module Prism
if node.requireds.any?
node.requireds.each do |required|
- if required.is_a?(RequiredParameterNode)
- params << visit(required)
- else
- compiler = copy_compiler(in_destructure: true)
- params << required.accept(compiler)
- end
+ params <<
+ if required.is_a?(RequiredParameterNode)
+ visit(required)
+ else
+ required.accept(copy_compiler(in_destructure: true))
+ end
end
end
@@ -1423,12 +1404,12 @@ module Prism
if node.posts.any?
node.posts.each do |post|
- if post.is_a?(RequiredParameterNode)
- params << visit(post)
- else
- compiler = copy_compiler(in_destructure: true)
- params << post.accept(compiler)
- end
+ params <<
+ if post.is_a?(RequiredParameterNode)
+ visit(post)
+ else
+ post.accept(copy_compiler(in_destructure: true))
+ end
end
end
@@ -1514,7 +1495,7 @@ module Prism
# 1r
# ^^
def visit_rational_node(node)
- visit_numeric(node, builder.rational([rational_value(node), srange(node.location)]))
+ visit_numeric(node, builder.rational([node.value, srange(node.location)]))
end
# redo
@@ -1526,9 +1507,20 @@ module Prism
# /foo/
# ^^^^^
def visit_regular_expression_node(node)
+ content = node.content
+ parts =
+ if content.include?("\n")
+ offset = node.content_loc.start_offset
+ content.lines.map do |line|
+ builder.string_internal([line, srange_offsets(offset, offset += line.bytesize)])
+ end
+ else
+ [builder.string_internal(token(node.content_loc))]
+ end
+
builder.regexp_compose(
token(node.opening_loc),
- [builder.string_internal(token(node.content_loc))],
+ parts,
[node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)],
builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)])
)
@@ -1674,10 +1666,11 @@ module Prism
# ^^^^^
def visit_string_node(node)
if node.heredoc?
- children, closing = visit_heredoc(node.to_interpolated)
- builder.string_compose(token(node.opening_loc), children, closing)
+ visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) }
elsif node.opening == "?"
builder.character([node.unescaped, srange(node.location)])
+ elsif node.opening&.start_with?("%") && node.unescaped.empty?
+ builder.string_compose(token(node.opening_loc), [], token(node.closing_loc))
else
content_lines = node.content.lines
unescaped_lines = node.unescaped.lines
@@ -1791,16 +1784,16 @@ module Prism
if node.then_keyword_loc
token(node.then_keyword_loc)
else
- srange_find(node.predicate.location.end_offset, (node.statements&.location || node.consequent&.location || node.end_keyword_loc).start_offset, [";"])
+ srange_find(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset, [";"])
end,
- visit(node.consequent),
- token(node.consequent&.else_keyword_loc),
+ visit(node.else_clause),
+ token(node.else_clause&.else_keyword_loc),
visit(node.statements),
token(node.end_keyword_loc)
)
else
builder.condition_mod(
- visit(node.consequent),
+ visit(node.else_clause),
visit(node.statements),
token(node.keyword_loc),
visit(node.predicate)
@@ -1877,8 +1870,7 @@ module Prism
# ^^^^^
def visit_x_string_node(node)
if node.heredoc?
- children, closing = visit_heredoc(node.to_interpolated)
- builder.xstring_compose(token(node.opening_loc), children, closing)
+ visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) }
else
parts = if node.unescaped.lines.one?
[builder.string_internal([node.unescaped, srange(node.content_loc)])]
@@ -1940,10 +1932,12 @@ module Prism
forwarding
end
- # Because we have mutated the AST to allow for newlines in the middle of
- # a rational, we need to manually handle the value here.
- def imaginary_value(node)
- Complex(0, node.numeric.is_a?(RationalNode) ? rational_value(node.numeric) : node.numeric.value)
+ # Returns the set of targets for a MultiTargetNode or a MultiWriteNode.
+ def multi_target_elements(node)
+ elements = [*node.lefts]
+ elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode)
+ elements.concat(node.rights)
+ elements
end
# Negate the value of a numeric node. This is a special case where you
@@ -1955,7 +1949,9 @@ module Prism
case receiver.type
when :integer_node, :float_node
receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location))
- when :rational_node, :imaginary_node
+ when :rational_node
+ receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location))
+ when :imaginary_node
receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location))
end
end
@@ -1974,16 +1970,6 @@ module Prism
parameters.block.nil?
end
- # Because we have mutated the AST to allow for newlines in the middle of
- # a rational, we need to manually handle the value here.
- def rational_value(node)
- if node.numeric.is_a?(IntegerNode)
- Rational(node.numeric.value)
- else
- Rational(node.slice.gsub(/\s/, "").chomp("r"))
- end
- end
-
# Locations in the parser gem AST are generated using this class. We
# store a reference to its constant to make it slightly faster to look
# up.
@@ -2006,7 +1992,7 @@ module Prism
# Note that end_offset is allowed to be nil, in which case this will
# search until the end of the string.
def srange_find(start_offset, end_offset, tokens)
- if (match = source_buffer.source.byteslice(start_offset...end_offset).match(/(\s*)(#{tokens.join("|")})/))
+ if (match = source_buffer.source.byteslice(start_offset...end_offset).match(/\A(\s*)(#{tokens.join("|")})/))
_, whitespace, token = *match
token_offset = start_offset + whitespace.bytesize
@@ -2037,7 +2023,8 @@ module Prism
token(parameters.opening_loc),
if procarg0?(parameters.parameters)
parameter = parameters.parameters.requireds.first
- [builder.procarg0(visit(parameter))].concat(visit_all(parameters.locals))
+ visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true))
+ [builder.procarg0(visited)].concat(visit_all(parameters.locals))
else
visit(parameters)
end,
@@ -2053,29 +2040,55 @@ module Prism
end
end
+ # The parser gem automatically converts \r\n to \n, meaning our offsets
+ # need to be adjusted to always subtract 1 from the length.
+ def chomped_bytesize(line)
+ chomped = line.chomp
+ chomped.bytesize + (chomped == line ? 0 : 1)
+ end
+
# Visit a heredoc that can be either a string or an xstring.
def visit_heredoc(node)
children = Array.new
+ indented = false
+
+ # If this is a dedenting heredoc, then we need to insert the opening
+ # content into the children as well.
+ if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode)
+ location = node.parts.first.location
+ location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize)
+ children << builder.string_internal(token(location))
+ indented = true
+ end
+
node.parts.each do |part|
pushing =
if part.is_a?(StringNode) && part.unescaped.include?("\n")
- unescaped = part.unescaped.lines(chomp: true)
- escaped = part.content.lines(chomp: true)
+ unescaped = part.unescaped.lines
+ escaped = part.content.lines
- escaped_lengths =
- if node.opening.end_with?("'")
- escaped.map { |line| line.bytesize + 1 }
- else
- escaped.chunk_while { |before, after| before.match?(/(?<!\\)\\$/) }.map { |line| line.join.bytesize + line.length }
+ escaped_lengths = []
+ normalized_lengths = []
+
+ if node.opening.end_with?("'")
+ escaped.each do |line|
+ escaped_lengths << line.bytesize
+ normalized_lengths << chomped_bytesize(line)
end
+ else
+ escaped
+ .chunk_while { |before, after| before.match?(/(?<!\\)\\\r?\n$/) }
+ .each do |lines|
+ escaped_lengths << lines.sum(&:bytesize)
+ normalized_lengths << lines.sum { |line| chomped_bytesize(line) }
+ end
+ end
start_offset = part.location.start_offset
- end_offset = nil
- unescaped.zip(escaped_lengths).map do |unescaped_line, escaped_length|
- end_offset = start_offset + (escaped_length || 0)
- inner_part = builder.string_internal(["#{unescaped_line}\n", srange_offsets(start_offset, end_offset)])
- start_offset = end_offset
+ unescaped.map.with_index do |unescaped_line, index|
+ inner_part = builder.string_internal([unescaped_line, srange_offsets(start_offset, start_offset + normalized_lengths.fetch(index, 0))])
+ start_offset += escaped_lengths.fetch(index, 0)
inner_part
end
else
@@ -2086,7 +2099,12 @@ module Prism
if child.type == :str && child.children.last == ""
# nothing
elsif child.type == :str && children.last && children.last.type == :str && !children.last.children.first.end_with?("\n")
- children.last.children.first << child.children.first
+ appendee = children[-1]
+
+ location = appendee.loc
+ location = location.with_expression(location.expression.join(child.loc.expression))
+
+ children[-1] = appendee.updated(:str, [appendee.children.first << child.children.first], location: location)
else
children << child
end
@@ -2095,8 +2113,10 @@ module Prism
closing = node.closing
closing_t = [closing.chomp, srange_offsets(node.closing_loc.start_offset, node.closing_loc.end_offset - (closing[/\s+$/]&.length || 0))]
+ composed = yield children, closing_t
- [children, closing_t]
+ composed = composed.updated(nil, children[1..-1]) if indented
+ composed
end
# Visit a numeric node and account for the optional sign.
diff --git a/lib/prism/translation/parser/lexer.rb b/lib/prism/translation/parser/lexer.rb
index 9d7caae0ba..db7dbb1c87 100644
--- a/lib/prism/translation/parser/lexer.rb
+++ b/lib/prism/translation/parser/lexer.rb
@@ -134,7 +134,7 @@ module Prism
MINUS_GREATER: :tLAMBDA,
NEWLINE: :tNL,
NUMBERED_REFERENCE: :tNTH_REF,
- PARENTHESIS_LEFT: :tLPAREN,
+ PARENTHESIS_LEFT: :tLPAREN2,
PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG,
PARENTHESIS_RIGHT: :tRPAREN,
PERCENT: :tPERCENT,
@@ -173,7 +173,7 @@ module Prism
UMINUS_NUM: :tUNARY_NUM,
UPLUS: :tUPLUS,
USTAR: :tSTAR,
- USTAR_STAR: :tPOW,
+ USTAR_STAR: :tDSTAR,
WORDS_SEP: :tSPACE
}
@@ -187,7 +187,20 @@ module Prism
EXPR_BEG = 0x1 # :nodoc:
EXPR_LABEL = 0x400 # :nodoc:
- private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL
+ # It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`.
+ #
+ # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned
+ # instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046
+ LAMBDA_TOKEN_TYPES = [:kDO_LAMBDA, :tLAMBDA, :tLAMBEG]
+
+ # The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem.
+ # The following token types are listed as those classified as `tLPAREN`.
+ LPAREN_CONVERSION_TOKEN_TYPES = [
+ :kBREAK, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3,
+ :tEQL, :tLPAREN, :tLPAREN2, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS
+ ]
+
+ private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LAMBDA_TOKEN_TYPES, :LPAREN_CONVERSION_TOKEN_TYPES
# The Parser::Source::Buffer that the tokens were lexed from.
attr_reader :source_buffer
@@ -229,6 +242,13 @@ module Prism
location = Range.new(source_buffer, offset_cache[token.location.start_offset], offset_cache[token.location.end_offset])
case type
+ when :kDO
+ types = tokens.map(&:first)
+ nearest_lambda_token_type = types.reverse.find { |type| LAMBDA_TOKEN_TYPES.include?(type) }
+
+ if nearest_lambda_token_type == :tLAMBDA
+ type = :kDO_LAMBDA
+ end
when :tCHARACTER
value.delete_prefix!("?")
when :tCOMMENT
@@ -268,6 +288,8 @@ module Prism
value.chomp!(":")
when :tLCURLY
type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL
+ when :tLPAREN2
+ type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0))
when :tNTH_REF
value = parse_integer(value.delete_prefix("$"))
when :tOP_ASGN
@@ -339,7 +361,7 @@ module Prism
location = Range.new(source_buffer, offset_cache[token.location.start_offset], offset_cache[token.location.start_offset + 1])
end
when :tSYMBEG
- if (next_token = lexed[index][0]) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR
+ if (next_token = lexed[index][0]) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR && next_token.type != :STRING_END
next_location = token.location.join(next_token.location)
type = :tSYMBOL
value = next_token.value
diff --git a/lib/prism/translation/parser/rubocop.rb b/lib/prism/translation/parser/rubocop.rb
deleted file mode 100644
index 6c9687a5cc..0000000000
--- a/lib/prism/translation/parser/rubocop.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-# typed: ignore
-
-warn "WARN: Prism is directly supported since RuboCop 1.62. The `prism/translation/parser/rubocop` file is deprecated."
-
-require "parser"
-require "rubocop"
-
-require_relative "../../prism"
-require_relative "../parser"
-
-module Prism
- module Translation
- class Parser
- # This is the special version numbers that should be used in RuboCop
- # configuration files to trigger using prism.
-
- # For Ruby 3.3
- VERSION_3_3 = 80_82_73_83_77.33
-
- # For Ruby 3.4
- VERSION_3_4 = 80_82_73_83_77.34
-
- # This module gets prepended into RuboCop::AST::ProcessedSource.
- module ProcessedSource
- # This condition is compatible with rubocop-ast versions up to 1.30.0.
- if RuboCop::AST::ProcessedSource.instance_method(:parser_class).arity == 1
- # Redefine parser_class so that we can inject the prism parser into the
- # list of known parsers.
- def parser_class(ruby_version)
- if ruby_version == Prism::Translation::Parser::VERSION_3_3
- warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.33` is deprecated. " \
- "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.3` instead."
- require_relative "../parser33"
- Prism::Translation::Parser33
- elsif ruby_version == Prism::Translation::Parser::VERSION_3_4
- warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.34` is deprecated. " \
- "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.4` instead."
- require_relative "../parser34"
- Prism::Translation::Parser34
- else
- super
- end
- end
- else
- # Redefine parser_class so that we can inject the prism parser into the
- # list of known parsers.
- def parser_class(ruby_version, _parser_engine)
- if ruby_version == Prism::Translation::Parser::VERSION_3_3
- warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.33` is deprecated. " \
- "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.3` instead."
- require_relative "../parser33"
- Prism::Translation::Parser33
- elsif ruby_version == Prism::Translation::Parser::VERSION_3_4
- warn "WARN: Setting `TargetRubyVersion: 80_82_73_83_77.34` is deprecated. " \
- "Set to `ParserEngine: parser_prism` and `TargetRubyVersion: 3.4` instead."
- require_relative "../parser34"
- Prism::Translation::Parser34
- else
- super
- end
- end
- end
- end
- end
- end
-end
-
-# :stopdoc:
-RuboCop::AST::ProcessedSource.prepend(Prism::Translation::Parser::ProcessedSource)
-known_rubies = RuboCop::TargetRuby.const_get(:KNOWN_RUBIES)
-RuboCop::TargetRuby.send(:remove_const, :KNOWN_RUBIES)
-RuboCop::TargetRuby::KNOWN_RUBIES = [*known_rubies, Prism::Translation::Parser::VERSION_3_3].freeze
diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb
index 68f658565d..cafe8c3f63 100644
--- a/lib/prism/translation/ripper.rb
+++ b/lib/prism/translation/ripper.rb
@@ -1273,8 +1273,8 @@ module Prism
def visit_case_node(node)
predicate = visit(node.predicate)
clauses =
- node.conditions.reverse_each.inject(visit(node.consequent)) do |consequent, condition|
- on_when(*visit(condition), consequent)
+ node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition|
+ on_when(*visit(condition), current)
end
bounds(node.location)
@@ -1286,8 +1286,8 @@ module Prism
def visit_case_match_node(node)
predicate = visit(node.predicate)
clauses =
- node.conditions.reverse_each.inject(visit(node.consequent)) do |consequent, condition|
- on_in(*visit(condition), consequent)
+ node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition|
+ on_in(*visit(condition), current)
end
bounds(node.location)
@@ -1908,7 +1908,7 @@ module Prism
if node.then_keyword == "?"
predicate = visit(node.predicate)
truthy = visit(node.statements.body.first)
- falsy = visit(node.consequent.statements.body.first)
+ falsy = visit(node.subsequent.statements.body.first)
bounds(node.location)
on_ifop(predicate, truthy, falsy)
@@ -1921,13 +1921,13 @@ module Prism
else
visit(node.statements)
end
- consequent = visit(node.consequent)
+ subsequent = visit(node.subsequent)
bounds(node.location)
if node.if_keyword == "if"
- on_if(predicate, statements, consequent)
+ on_if(predicate, statements, subsequent)
else
- on_elsif(predicate, statements, consequent)
+ on_elsif(predicate, statements, subsequent)
end
else
statements = visit(node.statements.body.first)
@@ -1960,7 +1960,7 @@ module Prism
# ^^^^^^^^^^^^^^^^^^^^^
def visit_in_node(node)
# This is a special case where we're not going to call on_in directly
- # because we don't have access to the consequent. Instead, we'll return
+ # because we don't have access to the subsequent. Instead, we'll return
# the component parts and let the parent node handle it.
pattern = visit_pattern_node(node.pattern)
statements =
@@ -2218,6 +2218,13 @@ module Prism
end
# -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ bounds(node.location)
+ on_vcall(on_ident(node.slice))
+ end
+
+ # -> { it }
# ^^^^^^^^^
def visit_it_parameters_node(node)
end
@@ -2312,12 +2319,7 @@ module Prism
# ^^^
def visit_local_variable_read_node(node)
bounds(node.location)
-
- if node.name == :"0it"
- on_vcall(on_ident(node.slice))
- else
- on_var_ref(on_ident(node.slice))
- end
+ on_var_ref(on_ident(node.slice))
end
# foo = 1
@@ -2806,10 +2808,10 @@ module Prism
visit(node.statements)
end
- consequent = visit(node.consequent)
+ subsequent = visit(node.subsequent)
bounds(node.location)
- on_rescue(exceptions, reference, statements, consequent)
+ on_rescue(exceptions, reference, statements, subsequent)
end
# def foo(*bar); end
@@ -3130,10 +3132,10 @@ module Prism
else
visit(node.statements)
end
- consequent = visit(node.consequent)
+ else_clause = visit(node.else_clause)
bounds(node.location)
- on_unless(predicate, statements, consequent)
+ on_unless(predicate, statements, else_clause)
else
statements = visit(node.statements.body.first)
predicate = visit(node.predicate)
@@ -3174,7 +3176,7 @@ module Prism
# ^^^^^^^^^^^^^
def visit_when_node(node)
# This is a special case where we're not going to call on_when directly
- # because we don't have access to the consequent. Instead, we'll return
+ # because we don't have access to the subsequent. Instead, we'll return
# the component parts and let the parent node handle it.
conditions = visit_arguments(node.conditions)
statements =
diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb
index 43aac23be7..4ccff0b600 100644
--- a/lib/prism/translation/ruby_parser.rb
+++ b/lib/prism/translation/ruby_parser.rb
@@ -55,7 +55,19 @@ module Prism
# a and b
# ^^^^^^^
def visit_and_node(node)
- s(node, :and, visit(node.left), visit(node.right))
+ left = visit(node.left)
+
+ if left[0] == :and
+ # ruby_parser has the and keyword as right-associative as opposed to
+ # prism which has it as left-associative. We reverse that
+ # associativity here.
+ nest = left
+ nest = nest[2] while nest[2][0] == :and
+ nest[2] = s(node, :and, nest[2], visit(node.right))
+ left
+ else
+ s(node, :and, left, visit(node.right))
+ end
end
# []
@@ -135,7 +147,7 @@ module Prism
end
current = node.rescue_clause
- until (current = current.consequent).nil?
+ until (current = current.subsequent).nil?
result << visit(current)
end
end
@@ -251,6 +263,11 @@ module Prism
when RegularExpressionNode, InterpolatedRegularExpressionNode
return s(node, :match2, visit(node.receiver), visit(node.arguments.arguments.first))
end
+
+ case node.arguments.arguments.first
+ when RegularExpressionNode, InterpolatedRegularExpressionNode
+ return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver))
+ end
end
end
@@ -330,13 +347,13 @@ module Prism
# case foo; when bar; end
# ^^^^^^^^^^^^^^^^^^^^^^^
def visit_case_node(node)
- s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.consequent)
+ s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause)
end
# case foo; in bar; end
# ^^^^^^^^^^^^^^^^^^^^^
def visit_case_match_node(node)
- s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.consequent)
+ s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause)
end
# class Foo; end
@@ -485,9 +502,9 @@ module Prism
def visit_constant_path_target_node(node)
inner =
if node.parent.nil?
- s(node, :colon3, node.child.name)
+ s(node, :colon3, node.name)
else
- s(node, :colon2, visit(node.parent), node.child.name)
+ s(node, :colon2, visit(node.parent), node.name)
end
s(node, :const, inner)
@@ -683,7 +700,7 @@ module Prism
# foo ? bar : baz
# ^^^^^^^^^^^^^^^
def visit_if_node(node)
- s(node, :if, visit(node.predicate), visit(node.statements), visit(node.consequent))
+ s(node, :if, visit(node.predicate), visit(node.statements), visit(node.subsequent))
end
# 1i
@@ -875,6 +892,15 @@ module Prism
else
visited << result
end
+ elsif result[0] == :dstr
+ if !visited.empty? && part.parts[0].is_a?(StringNode)
+ # If we are in the middle of an implicitly concatenated string,
+ # we should not have a bare string as the first part. In this
+ # case we need to visit just that first part and then we can
+ # push the rest of the parts onto the visited array.
+ result[1] = visit(part.parts[0])
+ end
+ visited.concat(result[1..-1])
else
visited << result
end
@@ -905,12 +931,23 @@ module Prism
results << result
state = :interpolated_content
end
- else
- results << result
+ when :interpolated_content
+ if result.is_a?(Array) && result[0] == :str && results[-1][0] == :str && (results[-1].line_max == result.line)
+ results[-1][1] << result[1]
+ results[-1].line_max = result.line_max
+ else
+ results << result
+ end
end
end
end
+ # -> { it }
+ # ^^
+ def visit_it_local_variable_read_node(node)
+ s(node, :call, nil, :it)
+ end
+
# foo(bar: baz)
# ^^^^^^^^
def visit_keyword_hash_node(node)
@@ -1123,7 +1160,19 @@ module Prism
# a or b
# ^^^^^^
def visit_or_node(node)
- s(node, :or, visit(node.left), visit(node.right))
+ left = visit(node.left)
+
+ if left[0] == :or
+ # ruby_parser has the or keyword as right-associative as opposed to
+ # prism which has it as left-associative. We reverse that
+ # associativity here.
+ nest = left
+ nest = nest[2] while nest[2][0] == :or
+ nest[2] = s(node, :or, nest[2], visit(node.right))
+ left
+ else
+ s(node, :or, left, visit(node.right))
+ end
end
# def foo(bar, *baz); end
@@ -1379,7 +1428,13 @@ module Prism
# "foo"
# ^^^^^
def visit_string_node(node)
- s(node, :str, node.unescaped)
+ unescaped = node.unescaped
+
+ if node.forced_binary_encoding?
+ unescaped.force_encoding(Encoding::BINARY)
+ end
+
+ s(node, :str, unescaped)
end
# super(foo)
@@ -1421,7 +1476,7 @@ module Prism
# bar unless foo
# ^^^^^^^^^^^^^^
def visit_unless_node(node)
- s(node, :if, visit(node.predicate), visit(node.consequent), visit(node.statements))
+ s(node, :if, visit(node.predicate), visit(node.else_clause), visit(node.statements))
end
# until foo; bar end
diff --git a/lib/random/formatter.rb b/lib/random/formatter.rb
index 037f9d8748..2b5cf718ad 100644
--- a/lib/random/formatter.rb
+++ b/lib/random/formatter.rb
@@ -165,7 +165,7 @@ module Random::Formatter
#
# The result contains 122 random bits (15.25 random bytes).
#
- # See RFC4122[https://www.rfc-editor.org/rfc/rfc4122] for details of UUID.
+ # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv4.
#
def uuid
ary = random_bytes(16)
@@ -204,8 +204,7 @@ module Random::Formatter
# Note that this method cannot be made reproducible because its output
# includes not only random bits but also timestamp.
#
- # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/]
- # for details of UUIDv7.
+ # See RFC9562[https://www.rfc-editor.org/rfc/rfc9562] for details of UUIDv7.
#
# ==== Monotonicity
#
@@ -242,7 +241,7 @@ module Random::Formatter
#
# Counters and other mechanisms for stronger guarantees of monotonicity are
# not implemented. Applications with stricter requirements should follow
- # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters]
+ # {Section 6.2}[https://www.rfc-editor.org/rfc/rfc9562.html#name-monotonicity-and-counters]
# of the specification.
#
def uuid_v7(extra_timestamp_bits: 0)
diff --git a/lib/rdoc.rb b/lib/rdoc.rb
index 9dc4595324..3821569f45 100644
--- a/lib/rdoc.rb
+++ b/lib/rdoc.rb
@@ -21,7 +21,7 @@ $DEBUG_RDOC = nil
# see RDoc::Markup and refer to <tt>rdoc --help</tt> for command line usage.
#
# If you want to set the default markup format see
-# RDoc::Markup@Supported+Formats
+# RDoc::Markup@Markup+Formats
#
# If you want to store rdoc configuration in your gem (such as the default
# markup format) see RDoc::Options@Saved+Options
@@ -188,26 +188,26 @@ module RDoc
# programs: classes, modules, methods, and so on.
autoload :CodeObject, "#{__dir__}/rdoc/code_object"
- autoload :Context, "#{__dir__}/rdoc/context"
- autoload :TopLevel, "#{__dir__}/rdoc/top_level"
-
- autoload :AnonClass, "#{__dir__}/rdoc/anon_class"
- autoload :ClassModule, "#{__dir__}/rdoc/class_module"
- autoload :NormalClass, "#{__dir__}/rdoc/normal_class"
- autoload :NormalModule, "#{__dir__}/rdoc/normal_module"
- autoload :SingleClass, "#{__dir__}/rdoc/single_class"
-
- autoload :Alias, "#{__dir__}/rdoc/alias"
- autoload :AnyMethod, "#{__dir__}/rdoc/any_method"
- autoload :MethodAttr, "#{__dir__}/rdoc/method_attr"
- autoload :GhostMethod, "#{__dir__}/rdoc/ghost_method"
- autoload :MetaMethod, "#{__dir__}/rdoc/meta_method"
- autoload :Attr, "#{__dir__}/rdoc/attr"
-
- autoload :Constant, "#{__dir__}/rdoc/constant"
- autoload :Mixin, "#{__dir__}/rdoc/mixin"
- autoload :Include, "#{__dir__}/rdoc/include"
- autoload :Extend, "#{__dir__}/rdoc/extend"
- autoload :Require, "#{__dir__}/rdoc/require"
+ autoload :Context, "#{__dir__}/rdoc/code_object/context"
+ autoload :TopLevel, "#{__dir__}/rdoc/code_object/top_level"
+
+ autoload :AnonClass, "#{__dir__}/rdoc/code_object/anon_class"
+ autoload :ClassModule, "#{__dir__}/rdoc/code_object/class_module"
+ autoload :NormalClass, "#{__dir__}/rdoc/code_object/normal_class"
+ autoload :NormalModule, "#{__dir__}/rdoc/code_object/normal_module"
+ autoload :SingleClass, "#{__dir__}/rdoc/code_object/single_class"
+
+ autoload :Alias, "#{__dir__}/rdoc/code_object/alias"
+ autoload :AnyMethod, "#{__dir__}/rdoc/code_object/any_method"
+ autoload :MethodAttr, "#{__dir__}/rdoc/code_object/method_attr"
+ autoload :GhostMethod, "#{__dir__}/rdoc/code_object/ghost_method"
+ autoload :MetaMethod, "#{__dir__}/rdoc/code_object/meta_method"
+ autoload :Attr, "#{__dir__}/rdoc/code_object/attr"
+
+ autoload :Constant, "#{__dir__}/rdoc/code_object/constant"
+ autoload :Mixin, "#{__dir__}/rdoc/code_object/mixin"
+ autoload :Include, "#{__dir__}/rdoc/code_object/include"
+ autoload :Extend, "#{__dir__}/rdoc/code_object/extend"
+ autoload :Require, "#{__dir__}/rdoc/code_object/require"
end
diff --git a/lib/rdoc/alias.rb b/lib/rdoc/code_object/alias.rb
index 446cf9ccb4..92df7e448f 100644
--- a/lib/rdoc/alias.rb
+++ b/lib/rdoc/code_object/alias.rb
@@ -70,7 +70,7 @@ class RDoc::Alias < RDoc::CodeObject
# HTML id-friendly version of +#new_name+.
def html_name
- CGI.escape(@new_name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '')
+ CGI.escape(@new_name.gsub('-', '-2D')).gsub('%', '-').sub(/^-/, '')
end
def inspect # :nodoc:
diff --git a/lib/rdoc/anon_class.rb b/lib/rdoc/code_object/anon_class.rb
index 3c2f0e1877..3c2f0e1877 100644
--- a/lib/rdoc/anon_class.rb
+++ b/lib/rdoc/code_object/anon_class.rb
diff --git a/lib/rdoc/any_method.rb b/lib/rdoc/code_object/any_method.rb
index 465c4a4fb2..465c4a4fb2 100644
--- a/lib/rdoc/any_method.rb
+++ b/lib/rdoc/code_object/any_method.rb
diff --git a/lib/rdoc/attr.rb b/lib/rdoc/code_object/attr.rb
index a403235933..a403235933 100644
--- a/lib/rdoc/attr.rb
+++ b/lib/rdoc/code_object/attr.rb
diff --git a/lib/rdoc/class_module.rb b/lib/rdoc/code_object/class_module.rb
index c69e14b5e4..c69e14b5e4 100644
--- a/lib/rdoc/class_module.rb
+++ b/lib/rdoc/code_object/class_module.rb
diff --git a/lib/rdoc/constant.rb b/lib/rdoc/code_object/constant.rb
index 12b8be775c..12b8be775c 100644
--- a/lib/rdoc/constant.rb
+++ b/lib/rdoc/code_object/constant.rb
diff --git a/lib/rdoc/context.rb b/lib/rdoc/code_object/context.rb
index c688d562c3..c688d562c3 100644
--- a/lib/rdoc/context.rb
+++ b/lib/rdoc/code_object/context.rb
diff --git a/lib/rdoc/context/section.rb b/lib/rdoc/code_object/context/section.rb
index aecd4e0213..aecd4e0213 100644
--- a/lib/rdoc/context/section.rb
+++ b/lib/rdoc/code_object/context/section.rb
diff --git a/lib/rdoc/extend.rb b/lib/rdoc/code_object/extend.rb
index 7d57433de6..7d57433de6 100644
--- a/lib/rdoc/extend.rb
+++ b/lib/rdoc/code_object/extend.rb
diff --git a/lib/rdoc/ghost_method.rb b/lib/rdoc/code_object/ghost_method.rb
index 25f951e35e..25f951e35e 100644
--- a/lib/rdoc/ghost_method.rb
+++ b/lib/rdoc/code_object/ghost_method.rb
diff --git a/lib/rdoc/include.rb b/lib/rdoc/code_object/include.rb
index c3e0d45e47..c3e0d45e47 100644
--- a/lib/rdoc/include.rb
+++ b/lib/rdoc/code_object/include.rb
diff --git a/lib/rdoc/meta_method.rb b/lib/rdoc/code_object/meta_method.rb
index 8c95a0f78c..8c95a0f78c 100644
--- a/lib/rdoc/meta_method.rb
+++ b/lib/rdoc/code_object/meta_method.rb
diff --git a/lib/rdoc/method_attr.rb b/lib/rdoc/code_object/method_attr.rb
index 61ddb32f46..27e6599bc1 100644
--- a/lib/rdoc/method_attr.rb
+++ b/lib/rdoc/code_object/method_attr.rb
@@ -268,8 +268,8 @@ class RDoc::MethodAttr < RDoc::CodeObject
when 'const_get' then 'const'
when 'new' then
$1.split('::').last. # ClassName => class_name
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
downcase
else
$2
@@ -291,7 +291,7 @@ class RDoc::MethodAttr < RDoc::CodeObject
def html_name
require 'cgi/util'
- CGI.escape(@name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '')
+ CGI.escape(@name.gsub('-', '-2D')).gsub('%', '-').sub(/^-/, '')
end
##
diff --git a/lib/rdoc/mixin.rb b/lib/rdoc/code_object/mixin.rb
index fa8faefc15..fa8faefc15 100644
--- a/lib/rdoc/mixin.rb
+++ b/lib/rdoc/code_object/mixin.rb
diff --git a/lib/rdoc/normal_class.rb b/lib/rdoc/code_object/normal_class.rb
index aa340b5d15..aa340b5d15 100644
--- a/lib/rdoc/normal_class.rb
+++ b/lib/rdoc/code_object/normal_class.rb
diff --git a/lib/rdoc/normal_module.rb b/lib/rdoc/code_object/normal_module.rb
index 498ec4dde2..498ec4dde2 100644
--- a/lib/rdoc/normal_module.rb
+++ b/lib/rdoc/code_object/normal_module.rb
diff --git a/lib/rdoc/require.rb b/lib/rdoc/code_object/require.rb
index 05e26b84b0..05e26b84b0 100644
--- a/lib/rdoc/require.rb
+++ b/lib/rdoc/code_object/require.rb
diff --git a/lib/rdoc/single_class.rb b/lib/rdoc/code_object/single_class.rb
index dd16529648..dd16529648 100644
--- a/lib/rdoc/single_class.rb
+++ b/lib/rdoc/code_object/single_class.rb
diff --git a/lib/rdoc/top_level.rb b/lib/rdoc/code_object/top_level.rb
index 3864f66431..b93e3802fc 100644
--- a/lib/rdoc/top_level.rb
+++ b/lib/rdoc/code_object/top_level.rb
@@ -183,8 +183,8 @@ class RDoc::TopLevel < RDoc::Context
"#<%s:0x%x %p modules: %p classes: %p>" % [
self.class, object_id,
base_name,
- @modules.map { |n,m| m },
- @classes.map { |n,c| c }
+ @modules.map { |n, m| m },
+ @classes.map { |n, c| c }
]
end
@@ -254,8 +254,8 @@ class RDoc::TopLevel < RDoc::Context
q.text "base name: #{base_name.inspect}"
q.breakable
- items = @modules.map { |n,m| m }
- items.concat @modules.map { |n,c| c }
+ items = @modules.map { |n, m| m }
+ items.concat @modules.map { |n, c| c }
q.seplist items do |mod| q.pp mod end
end
end
diff --git a/lib/rdoc/generator/darkfish.rb b/lib/rdoc/generator/darkfish.rb
index 1b408a6f8e..96bb4fb66f 100644
--- a/lib/rdoc/generator/darkfish.rb
+++ b/lib/rdoc/generator/darkfish.rb
@@ -677,7 +677,6 @@ class RDoc::Generator::Darkfish
return body if body =~ /<html/
head_file = @template_dir + '_head.rhtml'
- footer_file = @template_dir + '_footer.rhtml'
<<-TEMPLATE
<!DOCTYPE html>
@@ -687,8 +686,6 @@ class RDoc::Generator::Darkfish
#{head_file.read}
#{body}
-
-#{footer_file.read}
TEMPLATE
end
diff --git a/lib/rdoc/generator/pot/message_extractor.rb b/lib/rdoc/generator/pot/message_extractor.rb
index 313dfd2dc7..4938858bdc 100644
--- a/lib/rdoc/generator/pot/message_extractor.rb
+++ b/lib/rdoc/generator/pot/message_extractor.rb
@@ -29,7 +29,7 @@ class RDoc::Generator::POT::MessageExtractor
extract_text(klass.comment_location, klass.full_name)
klass.each_section do |section, constants, attributes|
- extract_text(section.title ,"#{klass.full_name}: section title")
+ extract_text(section.title, "#{klass.full_name}: section title")
section.comments.each do |comment|
extract_text(comment, "#{klass.full_name}: #{section.title}")
end
diff --git a/lib/rdoc/generator/pot/po_entry.rb b/lib/rdoc/generator/pot/po_entry.rb
index 3c278826f4..7454b29273 100644
--- a/lib/rdoc/generator/pot/po_entry.rb
+++ b/lib/rdoc/generator/pot/po_entry.rb
@@ -23,7 +23,7 @@ class RDoc::Generator::POT::POEntry
attr_reader :flags
##
- # Creates a PO entry for +msgid+. Other valus can be specified by
+ # Creates a PO entry for +msgid+. Other values can be specified by
# +options+.
def initialize msgid, options = {}
diff --git a/lib/rdoc/generator/template/darkfish/_head.rhtml b/lib/rdoc/generator/template/darkfish/_head.rhtml
index d5aed3e9ef..69649ad3b5 100644
--- a/lib/rdoc/generator/template/darkfish/_head.rhtml
+++ b/lib/rdoc/generator/template/darkfish/_head.rhtml
@@ -1,4 +1,5 @@
<meta charset="<%= @options.charset %>">
+<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= h @title %></title>
diff --git a/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml b/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml
new file mode 100644
index 0000000000..ed2cbe31a0
--- /dev/null
+++ b/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml
@@ -0,0 +1,3 @@
+<div id="navigation-toggle" role="button" tabindex="0" aria-label="Toggle sidebar" aria-expanded="true" aria-controls="navigation">
+ <span aria-hidden="true">&#9776;</span>
+</div>
diff --git a/lib/rdoc/generator/template/darkfish/class.rhtml b/lib/rdoc/generator/template/darkfish/class.rhtml
index d6510336df..6c64ba6c98 100644
--- a/lib/rdoc/generator/template/darkfish/class.rhtml
+++ b/lib/rdoc/generator/template/darkfish/class.rhtml
@@ -1,19 +1,20 @@
<body id="top" role="document" class="<%= klass.type %>">
-<nav role="navigation">
+<%= render '_sidebar_toggle.rhtml' %>
+
+<nav id="navigation" role="navigation">
<div id="project-navigation">
<%= render '_sidebar_navigation.rhtml' %>
<%= render '_sidebar_search.rhtml' %>
</div>
<%= render '_sidebar_table_of_contents.rhtml' %>
+ <%= render '_sidebar_sections.rhtml' %>
+ <%= render '_sidebar_parent.rhtml' %>
+ <%= render '_sidebar_includes.rhtml' %>
+ <%= render '_sidebar_extends.rhtml' %>
+ <%= render '_sidebar_methods.rhtml' %>
- <div id="class-metadata">
- <%= render '_sidebar_sections.rhtml' %>
- <%= render '_sidebar_parent.rhtml' %>
- <%= render '_sidebar_includes.rhtml' %>
- <%= render '_sidebar_extends.rhtml' %>
- <%= render '_sidebar_methods.rhtml' %>
- </div>
+ <%= render '_footer.rhtml' %>
</nav>
<main role="main" aria-labelledby="<%=h klass.aref %>">
diff --git a/lib/rdoc/generator/template/darkfish/css/rdoc.css b/lib/rdoc/generator/template/darkfish/css/rdoc.css
index 2cc55e03b1..169a6331e9 100644
--- a/lib/rdoc/generator/template/darkfish/css/rdoc.css
+++ b/lib/rdoc/generator/template/darkfish/css/rdoc.css
@@ -9,24 +9,42 @@
/* vim: ft=css et sw=2 ts=2 sts=2 */
/* Base Green is: #6C8C22 */
-.hide { display: none !important; }
-
-* { padding: 0; margin: 0; }
-
+/* 1. Variables and Root Styles */
+:root {
+ --sidebar-width: 300px;
+ --primary-color: #2c8c5e; /* A more muted green */
+ --secondary-color: #246b4b; /* A darker, muted green */
+ --text-color: #333;
+ --background-color: #f8f9fa;
+ --code-block-background-color: #f0f4f8;
+ --code-block-border-color: #d1d5da;
+ --link-color: #246b4b; /* A muted teal-green color */
+ --link-hover-color: #25a28a; /* A slightly brighter teal-green */
+ --border-color: #e0e0e0;
+ --sidebar-text-color: #2c3e50; /* Dark blue-gray for contrast */
+
+ /* Font family variables */
+ --font-primary: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
+ --font-heading: 'Helvetica Neue', Arial, sans-serif;
+ --font-code: 'Source Code Pro', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+}
+
+/* 2. Global Styles */
body {
- background: #fafafa;
- font-family: Lato, sans-serif;
- font-weight: 300;
+ background: var(--background-color);
+ font-family: var(--font-primary);
+ font-weight: 400;
+ color: var(--text-color);
+ line-height: 1.6;
/* Layout */
- display: grid;
- grid-template-columns: auto 1fr;
-}
-
-body > :last-child {
- grid-column: 1 / 3;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ margin: 0;
}
+/* 3. Typography */
h1 span,
h2 span,
h3 span,
@@ -68,36 +86,36 @@ h6:target {
border-left: 10px solid #f1edba;
}
+/* 4. Links */
:link,
:visited {
- color: #6C8C22;
+ color: var(--link-color);
text-decoration: none;
+ transition: color 0.3s ease;
+ font-weight: 600; /* Make links bolder */
}
:link:hover,
:visited:hover {
- border-bottom: 1px dotted #6C8C22;
+ color: var(--link-hover-color);
+ text-decoration: underline;
}
+/* 5. Code and Pre */
code,
pre {
- font-family: "Source Code Pro", Monaco, monospace;
- background-color: rgba(27,31,35,0.05);
- padding: 0em 0.2em;
- border-radius: 0.2em;
-}
-
-em {
- text-decoration-color: rgba(52, 48, 64, 0.25);
- text-decoration-line: underline;
- text-decoration-style: dotted;
-}
-
-strong,
-em {
- background-color: rgba(158, 178, 255, 0.1);
-}
-
+ font-family: var(--font-code);
+ background-color: var(--code-block-background-color);
+ border: 1px solid var(--code-block-border-color);
+ border-radius: 6px;
+ padding: 16px;
+ overflow-x: auto;
+ font-size: 15px; /* Increased from 14px */
+ line-height: 1.5; /* Slightly increased for better readability with larger font */
+ margin: 1em 0; /* Add some vertical margin */
+}
+
+/* 6. Tables */
table {
margin: 0;
border-spacing: 0;
@@ -117,110 +135,142 @@ table tr:nth-child(even) td {
background-color: #f5f4f6;
}
-/* @group Generic Classes */
-
-.initially-hidden {
+/* 7. Navigation and Sidebar */
+nav {
+ font-family: var(--font-heading);
+ font-size: 16px;
+ border-right: 1px solid var(--border-color);
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: var(--sidebar-width);
+ background: var(--background-color);
+ overflow-y: auto;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ color: var(--sidebar-text-color);
+}
+
+nav[hidden] {
display: none;
}
-#search-field {
- width: 98%;
- background: white;
- border: none;
- height: 1.5em;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- text-align: left;
+nav footer {
+ padding: 1em;
+ border-top: 1px solid #ccc;
}
-#search-field:focus {
- background: #f1edba;
+
+nav .nav-section {
+ margin-top: 1em;
+ padding: 0 1em;
}
-#search-field:-moz-placeholder,
-#search-field::-webkit-input-placeholder {
- font-weight: bold;
- color: #666;
+
+nav h2 {
+ margin: 0 0 0.5em;
+ padding: 0.5em 0;
+ font-size: 1.2em;
+ color: var(--text-color);
+ border-bottom: 1px solid var(--border-color);
}
-.missing-docs {
- font-size: 120%;
- background: white url(../images/wrench_orange.png) no-repeat 4px center;
- color: #ccc;
- line-height: 2em;
- border: 1px solid #d00;
- opacity: 1;
- padding-left: 20px;
- text-indent: 24px;
- letter-spacing: 3px;
- font-weight: bold;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
+nav h3,
+#table-of-contents-navigation {
+ margin: 0;
+ padding: 0.5em 0;
+ font-size: 1em;
+ color: var(--text-color);
}
-.target-section {
- border: 2px solid #dcce90;
- border-left-width: 8px;
- padding: 0 1em;
- background: #fff3c2;
+nav ul,
+nav dl,
+nav p {
+ padding: 0;
+ list-style: none;
+ margin: 0.5em 0;
}
-/* @end */
+nav ul li {
+ margin-bottom: 0.3em;
+}
-/* @group Index Page, Standalone file pages */
-.table-of-contents ul {
- margin: 1em;
- list-style: none;
+nav ul ul {
+ padding-left: 1em;
}
-.table-of-contents ul ul {
- margin-top: 0.25em;
+nav ul ul ul {
+ padding-left: 1em;
}
-.table-of-contents ul :link,
-.table-of-contents ul :visited {
- font-size: 16px;
+nav ul ul ul ul {
+ padding-left: 1em;
}
-.table-of-contents li {
- margin-bottom: 0.25em;
+nav a {
+ color: var(--link-color);
+ text-decoration: none;
}
-.table-of-contents li .toc-toggle {
- width: 16px;
- height: 16px;
- background: url(../images/add.png) no-repeat;
+nav a:hover {
+ text-decoration: underline;
}
-.table-of-contents li .toc-toggle.open {
- background: url(../images/delete.png) no-repeat;
+#navigation-toggle {
+ z-index: 1000;
+ font-size: 2em;
+ display: block;
+ position: fixed;
+ top: 10px;
+ left: 20px;
+ cursor: pointer;
}
-/* @end */
+#navigation-toggle[aria-expanded="true"] {
+ top: 10px;
+ left: 250px;
+}
-/* @group Top-Level Structure */
+nav ul li details {
+ position: relative;
+ padding-right: 1.5em; /* Add space for the marker on the right */
+}
-nav {
- font-family: Helvetica, sans-serif;
- font-size: 14px;
- border-right: 1px solid #ccc;
- position: sticky;
- top: 0;
- overflow: auto;
+nav ul li details > summary {
+ list-style: none; /* Remove the default marker */
+ position: relative; /* So that the open/close triangle can position itself absolutely inside */
+}
- /* Layout */
- width: 260px; /* fallback */
- width: max(50px, 20vw);
- min-width: 50px;
- max-width: 80vw;
- height: calc(100vh - 100px); /* reduce the footer height */
- resize: horizontal;
+nav ul li details > summary::after {
+ content: '▶'; /* Unicode right-pointing triangle */
+ position: absolute;
+ font-size: 0.8em;
+ bottom: 0.1em;
+ margin-left: 0.3em;
+ transition: transform 0.2s ease;
+}
+
+nav ul li details[open] > summary::after {
+ transform: rotate(90deg); /* Rotate the triangle when open */
}
+/* 8. Main Content */
main {
+ flex: 1;
display: block;
- margin: 1em;
- min-width: 340px;
+ margin: 3em auto;
+ padding: 0 2em;
+ max-width: 800px;
font-size: 16px;
+ line-height: 1.6;
+ color: var(--text-color);
+ box-sizing: border-box;
+}
+
+@media (min-width: 1024px) {
+ main {
+ margin-left: var(--sidebar-width);
+ }
}
main h1,
@@ -229,11 +279,13 @@ main h3,
main h4,
main h5,
main h6 {
- font-family: Helvetica, sans-serif;
+ font-family: var(--font-heading);
}
-.table-of-contents main {
- margin-left: 2em;
+@media (min-width: 1024px) {
+ .table-of-contents main {
+ margin-left: 20em;
+ }
}
#validator-badges {
@@ -241,121 +293,123 @@ main h6 {
font-size: smaller;
}
-/* @end */
-
-/* @group navigation */
-nav {
- margin-bottom: 1em;
+/* 9. Search */
+#search-section {
+ padding: 1em;
+ background-color: var(--background-color);
+ border-bottom: 1px solid var(--border-color);
}
-nav .nav-section {
- margin-top: 2em;
- border-top: 2px solid #aaa;
- font-size: 90%;
- overflow: hidden;
+#search-field-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
}
-nav h2 {
- margin: 0;
- padding: 2px 8px 2px 8px;
- background-color: #e8e8e8;
- color: #555;
- font-size: 125%;
- text-align: center;
+#search-field {
+ width: 100%;
+ padding: 0.5em 1em 0.5em 2.5em;
+ border: 1px solid var(--border-color);
+ border-radius: 20px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s ease;
+ background-color: var(--background-color);
}
-nav h3,
-#table-of-contents-navigation {
- margin: 0;
- padding: 2px 8px 2px 8px;
- text-align: right;
- background-color: #e8e8e8;
- color: #555;
+#search-field:focus {
+ border-color: var(--primary-color);
}
-nav ul,
-nav dl,
-nav p {
- padding: 4px 8px 0;
- list-style: none;
+#search-field::placeholder {
+ color: var(--text-color);
+ opacity: 0.6;
}
-#project-navigation .nav-section {
- margin: 0;
- border-top: 0;
+#search-field-wrapper::before {
+ content: "\1F50D"; /* Unicode for magnifying glass */
+ position: absolute;
+ left: 0.75em;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 14px;
+ color: var(--text-color);
+ opacity: 0.6;
}
-#home-section h2 {
- text-align: center;
-}
+/* 10. Utility Classes */
+.hide { display: none !important; }
+.initially-hidden { display: none; }
-#table-of-contents-navigation {
- font-size: 1.2em;
- font-weight: bold;
- text-align: center;
+/* 11. Media Queries */
+@media (min-width: 1024px) {
+ /* Styles for larger screens */
}
-#search-section {
- margin-top: 0;
- border-top: 0;
+/* 12. Print Styles */
+@media print {
+ /* Print-specific styles */
}
-#search-field-wrapper {
- border-top: 1px solid #aaa;
- border-bottom: 1px solid #aaa;
- padding: 3px 8px;
- background-color: #e8e8e8;
- color: #555;
-}
+/* 13. Syntax Highlighting */
+.ruby-constant { color: #0366d6; } /* Bright blue for constants */
+.ruby-keyword { color: #d73a49; } /* Red for keywords */
+.ruby-ivar { color: #e36209; } /* Orange for instance variables */
+.ruby-operator { color: #005cc5; } /* Dark blue for operators */
+.ruby-identifier { color: #24292e; } /* Dark gray for identifiers */
+.ruby-node { color: #22863a; } /* Green for interpolation */
+.ruby-comment { color: #6a737d; } /* Gray for comments */
+.ruby-regexp { color: #032f62; } /* Navy for regular expressions */
+.ruby-value { color: #005cc5; } /* Dark blue for numeric values */
+.ruby-string { color: #22863a; } /* Green for strings */
-ul.link-list li {
- white-space: nowrap;
- line-height: 1.4em;
+code {
+ background-color: #f0f4f8; /* Match pre background */
+ padding: 0.2em 0.4em;
+ border-radius: 3px;
+ font-size: 85%;
}
-ul.link-list .type {
- font-size: 8px;
- text-transform: uppercase;
- color: white;
- background: #969696;
- padding: 2px 4px;
- -webkit-border-radius: 5px;
+em {
+ text-decoration-color: rgba(52, 48, 64, 0.25);
+ text-decoration-line: underline;
+ text-decoration-style: dotted;
}
-dl.note-list dt {
- float: left;
- margin-right: 1em;
+strong,
+em {
+ background-color: rgba(158, 178, 255, 0.1);
}
-.calls-super {
- background: url(../images/arrow_up.png) no-repeat right center;
+/* 14. Specific Component Styles */
+.table-of-contents ul {
+ margin: 1em;
+ list-style: none;
}
-.nav-section details > summary {
- display: block;
+.table-of-contents ul ul {
+ margin-top: 0.25em;
}
-.nav-section details > summary::-webkit-details-marker {
- display: none;
+.table-of-contents ul :link,
+.table-of-contents ul :visited {
+ font-size: 16px;
}
-.nav-section details > summary::before {
- content: "";
+.table-of-contents li {
+ margin-bottom: 0.25em;
}
-.nav-section details > summary::after {
- content: "\25B6"; /* BLACK RIGHT-POINTING TRIANGLE */
- font-size: 0.8em;
- margin-left: 0.4em;
+.table-of-contents li .toc-toggle {
+ width: 16px;
+ height: 16px;
+ background: url(../images/add.png) no-repeat;
}
-.nav-section details[open] > summary::after {
- content: "\25BD"; /* WHITE DOWN-POINTING TRIANGLE */
+.table-of-contents li .toc-toggle.open {
+ background: url(../images/delete.png) no-repeat;
}
-/* @end */
-
-/* @group Documentation Section */
main {
color: #333;
}
@@ -378,43 +432,43 @@ main sup {
main h1[class] {
margin-top: 0;
margin-bottom: 1em;
- font-size: 2em;
- color: #6C8C22;
+ font-size: 2.5em;
+ color: var(--primary-color);
}
main h1 {
- margin: 2em 0 0.5em;
- font-size: 1.7em;
+ margin: 1.5em 0 0.5em;
+ font-size: 2.2em;
+ color: var(--secondary-color);
}
main h2 {
- margin: 2em 0 0.5em;
- font-size: 1.5em;
+ margin: 1.3em 0 0.5em;
+ font-size: 1.8em;
+ color: var(--secondary-color);
}
main h3 {
- margin: 2em 0 0.5em;
- font-size: 1.2em;
+ margin: 1.2em 0 0.5em;
+ font-size: 1.5em;
+ color: var(--secondary-color);
}
main h4 {
- margin: 2em 0 0.5em;
- font-size: 1.1em;
-}
-
-main h5 {
- margin: 2em 0 0.5em;
- font-size: 1em;
+ margin: 1.1em 0 0.5em;
+ font-size: 1.3em;
+ color: var(--secondary-color);
}
-main h6 {
- margin: 2em 0 0.5em;
- font-size: 1em;
+main h5, main h6 {
+ margin: 1em 0 0.5em;
+ font-size: 1.1em;
+ color: var(--secondary-color);
}
main p {
- margin: 0 0 0.5em;
- line-height: 1.4em;
+ line-height: 1.5em;
+ font-weight: 400;
}
main pre {
@@ -449,6 +503,8 @@ main dl {
main dt {
margin-bottom: 0.5em;
+ margin-right: 1em;
+ float: left;
font-weight: bold;
}
@@ -495,7 +551,7 @@ main header h3 {
.constants-list > dl dt {
margin-bottom: 0.75em;
padding-left: 0;
- font-family: "Source Code Pro", Monaco, monospace;
+ font-family: var(--font-code);
font-size: 110%;
}
@@ -542,7 +598,7 @@ main .method-source-code.active-menu {
}
main .method-description .method-calls-super {
- color: #333;
+ color: var(--text-color);
font-weight: bold;
}
@@ -558,10 +614,10 @@ main .method-detail:target {
main .method-heading {
position: relative;
- font-family: "Source Code Pro", Monaco, monospace;
+ font-family: var(--font-code);
font-size: 110%;
font-weight: bold;
- color: #333;
+ color: var(--text-color);
}
main .method-heading :link,
main .method-heading :visited {
@@ -589,7 +645,7 @@ main .method-alias .method-heading {
main .method-description,
main .aliases {
margin-top: 0.75em;
- color: #333;
+ color: var(--text-color);
}
main .aliases {
@@ -613,38 +669,14 @@ main .attribute-access-type {
/* @end */
-/* @group Source Code */
-
-pre {
- margin: 0.5em 0;
- border: 1px dashed #999;
- padding: 0.5em;
- background: #262626;
- color: white;
- overflow: auto;
-}
-
-.ruby-constant { color: #7fffd4; background: transparent; }
-.ruby-keyword { color: #00ffff; background: transparent; }
-.ruby-ivar { color: #eedd82; background: transparent; }
-.ruby-operator { color: #00ffee; background: transparent; }
-.ruby-identifier { color: #ffdead; background: transparent; }
-.ruby-node { color: #ffa07a; background: transparent; }
-.ruby-comment { color: #dc0000; background: transparent; }
-.ruby-regexp { color: #ffa07a; background: transparent; }
-.ruby-value { color: #7fffd4; background: transparent; }
-
-/* @end */
-
-
/* @group search results */
#search-results {
- font-family: Lato, sans-serif;
+ font-family: var(--font-primary);
font-weight: 300;
}
#search-results .search-match {
- font-family: Helvetica, sans-serif;
+ font-family: var(--font-heading);
font-weight: normal;
}
@@ -680,8 +712,34 @@ pre {
#search-results pre {
margin: 0.5em;
- font-family: "Source Code Pro", Monaco, monospace;
+ font-family: var(--font-code);
}
+@media (max-width: 480px) {
+ nav {
+ width: 100%;
+ }
+
+ main {
+ margin: 1em auto;
+ padding: 0 1em;
+ max-width: 100%;
+ }
+
+ #navigation-toggle {
+ right: 10px;
+ left: auto;
+ }
+
+ #navigation-toggle[aria-expanded="true"] {
+ left: auto;
+ }
+
+ table {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+}
/* @end */
diff --git a/lib/rdoc/generator/template/darkfish/index.rhtml b/lib/rdoc/generator/template/darkfish/index.rhtml
index 423e225b68..a5c0dd54da 100644
--- a/lib/rdoc/generator/template/darkfish/index.rhtml
+++ b/lib/rdoc/generator/template/darkfish/index.rhtml
@@ -1,15 +1,16 @@
<body id="top" role="document" class="file">
-<nav role="navigation">
+<%= render '_sidebar_toggle.rhtml' %>
+
+<nav id="navigation" role="navigation">
<div id="project-navigation">
<%= render '_sidebar_navigation.rhtml' %>
-
<%= render '_sidebar_search.rhtml' %>
</div>
- <div id="project-metadata">
- <%= render '_sidebar_pages.rhtml' %>
- <%= render '_sidebar_classes.rhtml' %>
- </div>
+ <%= render '_sidebar_pages.rhtml' %>
+ <%= render '_sidebar_classes.rhtml' %>
+
+ <%= render '_footer.rhtml' %>
</nav>
<main role="main">
diff --git a/lib/rdoc/generator/template/darkfish/js/darkfish.js b/lib/rdoc/generator/template/darkfish/js/darkfish.js
index 19a85c54e1..bea0a5f1cb 100644
--- a/lib/rdoc/generator/template/darkfish/js/darkfish.js
+++ b/lib/rdoc/generator/template/darkfish/js/darkfish.js
@@ -90,8 +90,25 @@ function hookFocus() {
});
}
+function hookSidebar() {
+ var navigation = document.querySelector('#navigation');
+ var navigationToggle = document.querySelector('#navigation-toggle');
+
+ navigationToggle.addEventListener('click', function() {
+ navigation.hidden = !navigation.hidden;
+ navigationToggle.ariaExpanded = navigationToggle.ariaExpanded !== 'true';
+ });
+
+ var isSmallViewport = window.matchMedia("(max-width: 1024px)").matches;
+ if (isSmallViewport) {
+ navigation.hidden = true;
+ navigationToggle.ariaExpanded = false;
+ }
+}
+
document.addEventListener('DOMContentLoaded', function() {
hookSourceViews();
hookSearch();
hookFocus();
+ hookSidebar();
});
diff --git a/lib/rdoc/generator/template/darkfish/page.rhtml b/lib/rdoc/generator/template/darkfish/page.rhtml
index 4a6b006bb3..fb33eba6fd 100644
--- a/lib/rdoc/generator/template/darkfish/page.rhtml
+++ b/lib/rdoc/generator/template/darkfish/page.rhtml
@@ -1,18 +1,18 @@
<body id="top" role="document" class="file">
-<nav role="navigation">
+<%= render '_sidebar_toggle.rhtml' %>
+
+<nav id="navigation" role="navigation">
<div id="project-navigation">
<%= render '_sidebar_navigation.rhtml' %>
<%= render '_sidebar_search.rhtml' %>
</div>
<%= render '_sidebar_table_of_contents.rhtml' %>
+ <%= render '_sidebar_pages.rhtml' %>
- <div id="project-metadata">
- <%= render '_sidebar_pages.rhtml' %>
- </div>
+ <%= render '_footer.rhtml' %>
</nav>
<main role="main" aria-label="Page <%=h file.full_name%>">
<%= file.description %>
</main>
-
diff --git a/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml b/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml
index f0841572c3..098b589add 100644
--- a/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml
+++ b/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml
@@ -1,13 +1,16 @@
<body role="document">
-<nav role="navigation">
- <%= render '_sidebar_navigation.rhtml' %>
+<%= render '_sidebar_toggle.rhtml' %>
- <%= render '_sidebar_search.rhtml' %>
-
- <div id="project-metadata">
- <%= render '_sidebar_pages.rhtml' %>
- <%= render '_sidebar_classes.rhtml' %>
+<nav id="navigation" role="navigation">
+ <div id="project-navigation">
+ <%= render '_sidebar_navigation.rhtml' %>
+ <%= render '_sidebar_search.rhtml' %>
</div>
+
+ <%= render '_sidebar_pages.rhtml' %>
+ <%= render '_sidebar_classes.rhtml' %>
+
+ <%= render '_footer.rhtml' %>
</nav>
<main role="main">
@@ -15,4 +18,3 @@
<p><%= message %>
</main>
-
diff --git a/lib/rdoc/generator/template/darkfish/servlet_root.rhtml b/lib/rdoc/generator/template/darkfish/servlet_root.rhtml
index cab3092b17..373e0006d9 100644
--- a/lib/rdoc/generator/template/darkfish/servlet_root.rhtml
+++ b/lib/rdoc/generator/template/darkfish/servlet_root.rhtml
@@ -1,5 +1,7 @@
<body role="document">
-<nav role="navigation">
+<%= render '_sidebar_toggle.rhtml' %>
+
+<nav id="navigation" role="navigation">
<div id="project-navigation">
<div id="home-section" class="nav-section">
<h2>
@@ -10,7 +12,8 @@
<%= render '_sidebar_search.rhtml' %>
</div>
-<%= render '_sidebar_installed.rhtml' %>
+ <%= render '_sidebar_installed.rhtml' %>
+ <%= render '_footer.rhtml' %>
</nav>
<main role="main">
diff --git a/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml b/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml
index 54a376c9e5..2cd2207836 100644
--- a/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml
+++ b/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml
@@ -1,4 +1,15 @@
<body id="top" class="table-of-contents">
+<%= render '_sidebar_toggle.rhtml' %>
+
+<nav id="navigation" role="navigation">
+ <div id="project-navigation">
+ <%= render '_sidebar_navigation.rhtml' %>
+
+ <%= render '_sidebar_search.rhtml' %>
+ </div>
+
+ <%= render '_footer.rhtml' %>
+</nav>
<main role="main">
<h1 class="class"><%= h @title %></h1>
diff --git a/lib/rdoc/markdown.rb b/lib/rdoc/markdown.rb
index 5c72a5f224..881d19ecb4 100644
--- a/lib/rdoc/markdown.rb
+++ b/lib/rdoc/markdown.rb
@@ -6,7 +6,7 @@
# RDoc::Markdown as described by the [markdown syntax][syntax].
#
# To choose Markdown as your only default format see
-# RDoc::Options@Saved+Options for instructions on setting up a `.doc_options`
+# RDoc::Options@Saved+Options for instructions on setting up a `.rdoc_options`
# file to store your project default.
#
# ## Usage
@@ -1158,7 +1158,7 @@ class RDoc::Markdown
return _tmp
end
- # AtxHeading = AtxStart:s @Sp AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }
+ # AtxHeading = AtxStart:s @Spacechar+ AtxInline+:a (@Sp /#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }
def _AtxHeading
_save = self.pos
@@ -1169,12 +1169,22 @@ class RDoc::Markdown
self.pos = _save
break
end
- _tmp = _Sp()
+ _save1 = self.pos
+ _tmp = _Spacechar()
+ if _tmp
+ while true
+ _tmp = _Spacechar()
+ break unless _tmp
+ end
+ _tmp = true
+ else
+ self.pos = _save1
+ end
unless _tmp
self.pos = _save
break
end
- _save1 = self.pos
+ _save2 = self.pos
_ary = []
_tmp = apply(:_AtxInline)
if _tmp
@@ -1187,37 +1197,37 @@ class RDoc::Markdown
_tmp = true
@result = _ary
else
- self.pos = _save1
+ self.pos = _save2
end
a = @result
unless _tmp
self.pos = _save
break
end
- _save2 = self.pos
-
_save3 = self.pos
+
+ _save4 = self.pos
while true # sequence
_tmp = _Sp()
unless _tmp
- self.pos = _save3
+ self.pos = _save4
break
end
_tmp = scan(/\G(?-mix:#*)/)
unless _tmp
- self.pos = _save3
+ self.pos = _save4
break
end
_tmp = _Sp()
unless _tmp
- self.pos = _save3
+ self.pos = _save4
end
break
end # end sequence
unless _tmp
_tmp = true
- self.pos = _save2
+ self.pos = _save3
end
unless _tmp
self.pos = _save
@@ -16539,7 +16549,7 @@ class RDoc::Markdown
Rules[:_Plain] = rule_info("Plain", "Inlines:a { paragraph a }")
Rules[:_AtxInline] = rule_info("AtxInline", "!@Newline !(@Sp /\#*/ @Sp @Newline) Inline")
Rules[:_AtxStart] = rule_info("AtxStart", "< /\\\#{1,6}/ > { text.length }")
- Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Sp AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }")
+ Rules[:_AtxHeading] = rule_info("AtxHeading", "AtxStart:s @Spacechar+ AtxInline+:a (@Sp /\#*/ @Sp)? @Newline { RDoc::Markup::Heading.new(s, a.join) }")
Rules[:_SetextHeading] = rule_info("SetextHeading", "(SetextHeading1 | SetextHeading2)")
Rules[:_SetextBottom1] = rule_info("SetextBottom1", "/={1,}/ @Newline")
Rules[:_SetextBottom2] = rule_info("SetextBottom2", "/-{1,}/ @Newline")
diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb
index 6e93030965..3c29870d8a 100644
--- a/lib/rdoc/markup.rb
+++ b/lib/rdoc/markup.rb
@@ -10,19 +10,24 @@
# RDoc::Markup and other markup formats do no output formatting, this is
# handled by the RDoc::Markup::Formatter subclasses.
#
-# = Supported Formats
-#
-# Besides the RDoc::Markup format, the following formats are built in to RDoc:
-#
-# markdown::
-# The markdown format as described by
-# http://daringfireball.net/projects/markdown/. See RDoc::Markdown for
-# details on the parser and supported extensions.
-# rd::
-# The rdtool format. See RDoc::RD for details on the parser and format.
-# tomdoc::
-# The TomDoc format as described by http://tomdoc.org/. See RDoc::TomDoc
-# for details on the parser and supported extensions.
+# = Markup Formats
+#
+# +RDoc+ supports these markup formats:
+#
+# - +rdoc+:
+# the +RDoc+ markup format;
+# see RDoc::MarkupReference.
+# - +markdown+:
+# The +markdown+ markup format as described in
+# the {Markdown Guide}[https://www.markdownguide.org];
+# see RDoc::Markdown.
+# - +rd+:
+# the +rd+ markup format format;
+# see RDoc::RD.
+# - +tomdoc+:
+# the TomDoc format as described in
+# {TomDoc for Ruby}[http://tomdoc.org];
+# see RDoc::TomDoc.
#
# You can choose a markup format using the following methods:
#
diff --git a/lib/rdoc/markup/attribute_manager.rb b/lib/rdoc/markup/attribute_manager.rb
index f6eb06da95..ed014f255b 100644
--- a/lib/rdoc/markup/attribute_manager.rb
+++ b/lib/rdoc/markup/attribute_manager.rb
@@ -260,7 +260,7 @@ class RDoc::Markup::AttributeManager
def add_word_pair(start, stop, name, exclusive = false)
raise ArgumentError, "Word flags may not start with '<'" if
- start[0,1] == '<'
+ start[0, 1] == '<'
bitmap = @attributes.bitmap_for name
@@ -271,7 +271,7 @@ class RDoc::Markup::AttributeManager
@word_pair_map[pattern] = bitmap
end
- @protectable << start[0,1]
+ @protectable << start[0, 1]
@protectable.uniq!
@exclusive_bitmap |= bitmap if exclusive
diff --git a/lib/rdoc/markup/pre_process.rb b/lib/rdoc/markup/pre_process.rb
index 88078c9cef..979f2eadae 100644
--- a/lib/rdoc/markup/pre_process.rb
+++ b/lib/rdoc/markup/pre_process.rb
@@ -97,15 +97,18 @@ class RDoc::Markup::PreProcess
# RDoc::CodeObject#metadata for details.
def handle text, code_object = nil, &block
+ first_line = 1
if RDoc::Comment === text then
comment = text
text = text.text
+ first_line = comment.line || 1
end
# regexp helper (square brackets for optional)
# $1 $2 $3 $4 $5
# [prefix][\]:directive:[spaces][param]newline
- text = text.gsub(/^([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):(\w+):([ \t]*)(.+)?(\r?\n|$)/) do
+ text = text.lines.map.with_index(first_line) do |line, num|
+ next line unless line =~ /\A([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/
# skip something like ':toto::'
next $& if $4.empty? and $5 and $5[0, 1] == ':'
@@ -120,8 +123,8 @@ class RDoc::Markup::PreProcess
next "#{$1.strip}\n"
end
- handle_directive $1, $3, $5, code_object, text.encoding, &block
- end
+ handle_directive $1, $3, $5, code_object, text.encoding, num, &block
+ end.join
if comment then
comment.text = text
@@ -148,7 +151,7 @@ class RDoc::Markup::PreProcess
# When 1.8.7 support is ditched prefix can be defaulted to ''
def handle_directive prefix, directive, param, code_object = nil,
- encoding = nil
+ encoding = nil, line = nil
blankline = "#{prefix.strip}\n"
directive = directive.downcase
@@ -220,11 +223,11 @@ class RDoc::Markup::PreProcess
# remove parameter &block
code_object.params = code_object.params.sub(/,?\s*&\w+/, '') if code_object.params
- code_object.block_params = param
+ code_object.block_params = param || ''
blankline
else
- result = yield directive, param if block_given?
+ result = yield directive, param, line if block_given?
case result
when nil then
diff --git a/lib/rdoc/markup/to_bs.rb b/lib/rdoc/markup/to_bs.rb
index afd9d6e981..b7b73e73f7 100644
--- a/lib/rdoc/markup/to_bs.rb
+++ b/lib/rdoc/markup/to_bs.rb
@@ -24,7 +24,7 @@ class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc
def init_tags
add_tag :BOLD, '+b', '-b'
add_tag :EM, '+_', '-_'
- add_tag :TT, '' , '' # we need in_tt information maintained
+ add_tag :TT, '', '' # we need in_tt information maintained
end
##
diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb
index 7518e6cc54..2631d57364 100644
--- a/lib/rdoc/options.rb
+++ b/lib/rdoc/options.rb
@@ -233,9 +233,9 @@ class RDoc::Options
attr_accessor :main_page
##
- # The default markup format. The default is 'rdoc'. 'markdown', 'tomdoc'
- # and 'rd' are also built-in.
-
+ # The markup format.
+ # One of: +rdoc+ (the default), +markdown+, +rd+, +tomdoc+.
+ # See {Markup Formats}[rdoc-ref:RDoc::Markup@Markup+Formats].
attr_accessor :markup
##
@@ -683,7 +683,7 @@ Usage: #{opt.program_name} [options] [names...]
EOF
- parsers = Hash.new { |h,parser| h[parser] = [] }
+ parsers = Hash.new { |h, parser| h[parser] = [] }
RDoc::Parser.parsers.each do |regexp, parser|
parsers[parser.name.sub('RDoc::Parser::', '')] << regexp.source
diff --git a/lib/rdoc/parser.rb b/lib/rdoc/parser.rb
index 425105effa..76801ba377 100644
--- a/lib/rdoc/parser.rb
+++ b/lib/rdoc/parser.rb
@@ -166,7 +166,8 @@ class RDoc::Parser
# Finds and instantiates the correct parser for the given +file_name+ and
# +content+.
- def self.for top_level, file_name, content, options, stats
+ def self.for top_level, content, options, stats
+ file_name = top_level.absolute_name
return if binary? file_name
parser = use_markup content
diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb
index f8f238fd74..4050d7aa49 100644
--- a/lib/rdoc/parser/c.rb
+++ b/lib/rdoc/parser/c.rb
@@ -440,7 +440,7 @@ class RDoc::Parser::C < RDoc::Parser
# Scans #content for rb_include_module
def do_includes
- @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c,m|
+ @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c, m|
next unless cls = @classes[c]
m = @known_classes[m] || m
diff --git a/lib/rdoc/parser/changelog.rb b/lib/rdoc/parser/changelog.rb
index a046241870..12a50f8d0e 100644
--- a/lib/rdoc/parser/changelog.rb
+++ b/lib/rdoc/parser/changelog.rb
@@ -193,7 +193,7 @@ class RDoc::Parser::ChangeLog < RDoc::Parser
entries << [entry_name, entry_body] if entry_name
- entries.reject! do |(entry,_)|
+ entries.reject! do |(entry, _)|
entry == nil
end
@@ -221,7 +221,7 @@ class RDoc::Parser::ChangeLog < RDoc::Parser
module Git
##
- # Parses auxiliary info. Currentry `base-url` to expand
+ # Parses auxiliary info. Currently `base-url` to expand
# references is effective.
def parse_info(info)
diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
new file mode 100644
index 0000000000..05e98ad6c4
--- /dev/null
+++ b/lib/rdoc/parser/prism_ruby.rb
@@ -0,0 +1,1026 @@
+# frozen_string_literal: true
+
+require 'prism'
+require_relative 'ripper_state_lex'
+
+# Unlike lib/rdoc/parser/ruby.rb, this file is not based on rtags and does not contain code from
+# rtags.rb -
+# ruby-lex.rb - ruby lexcal analyzer
+# ruby-token.rb - ruby tokens
+
+# Parse and collect document from Ruby source code.
+# RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it.
+
+class RDoc::Parser::PrismRuby < RDoc::Parser
+
+ parse_files_matching(/\.rbw?$/) if ENV['RDOC_USE_PRISM_PARSER']
+
+ attr_accessor :visibility
+ attr_reader :container, :singleton
+
+ def initialize(top_level, file_name, content, options, stats)
+ super
+
+ content = handle_tab_width(content)
+
+ @size = 0
+ @token_listeners = nil
+ content = RDoc::Encoding.remove_magic_comment content
+ @content = content
+ @markup = @options.markup
+ @track_visibility = :nodoc != @options.visibility
+ @encoding = @options.encoding
+
+ @module_nesting = [top_level]
+ @container = top_level
+ @visibility = :public
+ @singleton = false
+ end
+
+ # Dive into another container
+
+ def with_container(container, singleton: false)
+ old_container = @container
+ old_visibility = @visibility
+ old_singleton = @singleton
+ @visibility = :public
+ @container = container
+ @singleton = singleton
+ unless singleton
+ @module_nesting.push container
+
+ # Need to update module parent chain to emulate Module.nesting.
+ # This mechanism is inaccurate and needs to be fixed.
+ container.parent = old_container
+ end
+ yield container
+ ensure
+ @container = old_container
+ @visibility = old_visibility
+ @singleton = old_singleton
+ @module_nesting.pop unless singleton
+ end
+
+ # Records the location of this +container+ in the file for this parser and
+ # adds it to the list of classes and modules in the file.
+
+ def record_location container # :nodoc:
+ case container
+ when RDoc::ClassModule then
+ @top_level.add_to_classes_or_modules container
+ end
+
+ container.record_location @top_level
+ end
+
+ # Scans this Ruby file for Ruby constructs
+
+ def scan
+ @tokens = RDoc::Parser::RipperStateLex.parse(@content)
+ @lines = @content.lines
+ result = Prism.parse(@content)
+ @program_node = result.value
+ @line_nodes = {}
+ prepare_line_nodes(@program_node)
+ prepare_comments(result.comments)
+ return if @top_level.done_documenting
+
+ @first_non_meta_comment = nil
+ if (_line_no, start_line, rdoc_comment = @unprocessed_comments.first)
+ @first_non_meta_comment = rdoc_comment if start_line < @program_node.location.start_line
+ end
+
+ @program_node.accept(RDocVisitor.new(self, @top_level, @store))
+ process_comments_until(@lines.size + 1)
+ end
+
+ def should_document?(code_object) # :nodoc:
+ return true unless @track_visibility
+ return false if code_object.parent&.document_children == false
+ code_object.document_self
+ end
+
+ # Assign AST node to a line.
+ # This is used to show meta-method source code in the documentation.
+
+ def prepare_line_nodes(node) # :nodoc:
+ case node
+ when Prism::CallNode, Prism::DefNode
+ @line_nodes[node.location.start_line] ||= node
+ end
+ node.compact_child_nodes.each do |child|
+ prepare_line_nodes(child)
+ end
+ end
+
+ # Prepares comments for processing. Comments are grouped into consecutive.
+ # Consecutive comment is linked to the next non-blank line.
+ #
+ # Example:
+ # 01| class A # modifier comment 1
+ # 02| def foo; end # modifier comment 2
+ # 03|
+ # 04| # consecutive comment 1 start_line: 4
+ # 05| # consecutive comment 1 linked to line: 7
+ # 06|
+ # 07| # consecutive comment 2 start_line: 7
+ # 08| # consecutive comment 2 linked to line: 10
+ # 09|
+ # 10| def bar; end # consecutive comment 2 linked to this line
+ # 11| end
+
+ def prepare_comments(comments)
+ current = []
+ consecutive_comments = [current]
+ @modifier_comments = {}
+ comments.each do |comment|
+ if comment.is_a? Prism::EmbDocComment
+ consecutive_comments << [comment] << (current = [])
+ elsif comment.location.start_line_slice.match?(/\S/)
+ @modifier_comments[comment.location.start_line] = RDoc::Comment.new(comment.slice, @top_level, :ruby)
+ elsif current.empty? || current.last.location.end_line + 1 == comment.location.start_line
+ current << comment
+ else
+ consecutive_comments << (current = [comment])
+ end
+ end
+ consecutive_comments.reject!(&:empty?)
+
+ # Example: line_no = 5, start_line = 2, comment_text = "# comment_start_line\n# comment\n"
+ # 1| class A
+ # 2| # comment_start_line
+ # 3| # comment
+ # 4|
+ # 5| def f; end # comment linked to this line
+ # 6| end
+ @unprocessed_comments = consecutive_comments.map! do |comments|
+ start_line = comments.first.location.start_line
+ line_no = comments.last.location.end_line + (comments.last.location.end_column == 0 ? 0 : 1)
+ texts = comments.map do |c|
+ c.is_a?(Prism::EmbDocComment) ? c.slice.lines[1...-1].join : c.slice
+ end
+ text = RDoc::Encoding.change_encoding(texts.join("\n"), @encoding) if @encoding
+ line_no += 1 while @lines[line_no - 1]&.match?(/\A\s*$/)
+ comment = RDoc::Comment.new(text, @top_level, :ruby)
+ comment.line = start_line
+ [line_no, start_line, comment]
+ end
+
+ # The first comment is special. It defines markup for the rest of the comments.
+ _, first_comment_start_line, first_comment_text = @unprocessed_comments.first
+ if first_comment_text && @lines[0...first_comment_start_line - 1].all? { |l| l.match?(/\A\s*$/) }
+ comment = RDoc::Comment.new(first_comment_text.text, @top_level, :ruby)
+ handle_consecutive_comment_directive(@container, comment)
+ @markup = comment.format
+ end
+ @unprocessed_comments.each do |_, _, comment|
+ comment.format = @markup
+ end
+ end
+
+ # Creates an RDoc::Method on +container+ from +comment+ if there is a
+ # Signature section in the comment
+
+ def parse_comment_tomdoc(container, comment, line_no, start_line)
+ return unless signature = RDoc::TomDoc.signature(comment)
+
+ name, = signature.split %r%[ \(]%, 2
+
+ meth = RDoc::GhostMethod.new comment.text, name
+ record_location(meth)
+ meth.line = start_line
+ meth.call_seq = signature
+ return unless meth.name
+
+ meth.start_collecting_tokens
+ node = @line_nodes[line_no]
+ tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)]
+ tokens.each { |token| meth.token_stream << token }
+
+ container.add_method meth
+ comment.remove_private
+ comment.normalize
+ meth.comment = comment
+ @stats.add_method meth
+ end
+
+ def handle_modifier_directive(code_object, line_no) # :nodoc:
+ comment = @modifier_comments[line_no]
+ @preprocess.handle(comment.text, code_object) if comment
+ end
+
+ def handle_consecutive_comment_directive(code_object, comment) # :nodoc:
+ return unless comment
+ @preprocess.handle(comment, code_object) do |directive, param|
+ case directive
+ when 'method', 'singleton-method',
+ 'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then
+ # handled elsewhere
+ ''
+ when 'section' then
+ @container.set_current_section(param, comment.dup)
+ comment.text = ''
+ break
+ end
+ end
+ comment.remove_private
+ end
+
+ def call_node_name_arguments(call_node) # :nodoc:
+ return [] unless call_node.arguments
+ call_node.arguments.arguments.map do |arg|
+ case arg
+ when Prism::SymbolNode
+ arg.value
+ when Prism::StringNode
+ arg.unescaped
+ end
+ end || []
+ end
+
+ # Handles meta method comments
+
+ def handle_meta_method_comment(comment, node)
+ is_call_node = node.is_a?(Prism::CallNode)
+ singleton_method = false
+ visibility = @visibility
+ attributes = rw = line_no = method_name = nil
+
+ processed_comment = comment.dup
+ @preprocess.handle(processed_comment, @container) do |directive, param, line|
+ case directive
+ when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor'
+ attributes = [param] if param
+ attributes ||= call_node_name_arguments(node) if is_call_node
+ rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R'
+ ''
+ when 'method'
+ method_name = param
+ line_no = line
+ ''
+ when 'singleton-method'
+ method_name = param
+ line_no = line
+ singleton_method = true
+ visibility = :public
+ ''
+ when 'section' then
+ @container.set_current_section(param, comment.dup)
+ return # If the comment contains :section:, it is not a meta method comment
+ end
+ end
+
+ if attributes
+ attributes.each do |attr|
+ a = RDoc::Attr.new(@container, attr, rw, processed_comment)
+ a.store = @store
+ a.line = line_no
+ a.singleton = @singleton
+ record_location(a)
+ @container.add_attribute(a)
+ a.visibility = visibility
+ end
+ elsif line_no || node
+ method_name ||= call_node_name_arguments(node).first if is_call_node
+ meth = RDoc::AnyMethod.new(@container, method_name)
+ meth.singleton = @singleton || singleton_method
+ handle_consecutive_comment_directive(meth, comment)
+ comment.normalize
+ comment.extract_call_seq(meth)
+ meth.comment = comment
+ if node
+ tokens = visible_tokens_from_location(node.location)
+ line_no = node.location.start_line
+ else
+ tokens = [file_line_comment_token(line_no)]
+ end
+ internal_add_method(
+ @container,
+ meth,
+ line_no: line_no,
+ visibility: visibility,
+ singleton: @singleton || singleton_method,
+ params: '()',
+ calls_super: false,
+ block_params: nil,
+ tokens: tokens
+ )
+ end
+ end
+
+ def normal_comment_treat_as_ghost_method_for_now?(comment_text, line_no) # :nodoc:
+ # Meta method comment should start with `##` but some comments does not follow this rule.
+ # For now, RDoc accepts them as a meta method comment if there is no node linked to it.
+ !@line_nodes[line_no] && comment_text.match?(/^#\s+:(method|singleton-method|attr|attr_reader|attr_writer|attr_accessor):/)
+ end
+
+ def handle_standalone_consecutive_comment_directive(comment, line_no, start_line) # :nodoc:
+ if @markup == 'tomdoc'
+ parse_comment_tomdoc(@container, comment, line_no, start_line)
+ return
+ end
+
+ if comment.text =~ /\A#\#$/ && comment != @first_non_meta_comment
+ node = @line_nodes[line_no]
+ handle_meta_method_comment(comment, node)
+ elsif normal_comment_treat_as_ghost_method_for_now?(comment.text, line_no) && comment != @first_non_meta_comment
+ handle_meta_method_comment(comment, nil)
+ else
+ handle_consecutive_comment_directive(@container, comment)
+ end
+ end
+
+ # Processes consecutive comments that were not linked to any documentable code until the given line number
+
+ def process_comments_until(line_no_until)
+ while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
+ line_no, start_line, rdoc_comment = @unprocessed_comments.shift
+ handle_standalone_consecutive_comment_directive(rdoc_comment, line_no, start_line)
+ end
+ end
+
+ # Skips all undocumentable consecutive comments until the given line number.
+ # Undocumentable comments are comments written inside `def` or inside undocumentable class/module
+
+ def skip_comments_until(line_no_until)
+ while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
+ @unprocessed_comments.shift
+ end
+ end
+
+ # Returns consecutive comment linked to the given line number
+
+ def consecutive_comment(line_no)
+ if @unprocessed_comments.first&.first == line_no
+ @unprocessed_comments.shift.last
+ end
+ end
+
+ def slice_tokens(start_pos, end_pos) # :nodoc:
+ start_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> start_pos) >= 0 }
+ end_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> end_pos) >= 0 }
+ tokens = @tokens[start_index...end_index]
+ tokens.pop if tokens.last&.kind == :on_nl
+ tokens
+ end
+
+ def file_line_comment_token(line_no) # :nodoc:
+ position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no - 1, 0, :on_comment)
+ position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
+ position_comment
+ end
+
+ # Returns tokens from the given location
+
+ def visible_tokens_from_location(location)
+ position_comment = file_line_comment_token(location.start_line)
+ newline_token = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
+ indent_token = RDoc::Parser::RipperStateLex::Token.new(location.start_line, 0, :on_sp, ' ' * location.start_character_column)
+ tokens = slice_tokens(
+ [location.start_line, location.start_character_column],
+ [location.end_line, location.end_character_column]
+ )
+ [position_comment, newline_token, indent_token, *tokens]
+ end
+
+ # Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
+
+ def change_method_visibility(names, visibility, singleton: @singleton)
+ new_methods = []
+ @container.methods_matching(names, singleton) do |m|
+ if m.parent != @container
+ m = m.dup
+ record_location(m)
+ new_methods << m
+ else
+ m.visibility = visibility
+ end
+ end
+ new_methods.each do |method|
+ case method
+ when RDoc::AnyMethod then
+ @container.add_method(method)
+ when RDoc::Attr then
+ @container.add_attribute(method)
+ end
+ method.visibility = visibility
+ end
+ end
+
+ # Handles `module_function :foo, :bar`
+
+ def change_method_to_module_function(names)
+ @container.set_visibility_for(names, :private, false)
+ new_methods = []
+ @container.methods_matching(names) do |m|
+ s_m = m.dup
+ record_location(s_m)
+ s_m.singleton = true
+ new_methods << s_m
+ end
+ new_methods.each do |method|
+ case method
+ when RDoc::AnyMethod then
+ @container.add_method(method)
+ when RDoc::Attr then
+ @container.add_attribute(method)
+ end
+ method.visibility = :public
+ end
+ end
+
+ # Handles `alias foo bar` and `alias_method :foo, :bar`
+
+ def add_alias_method(old_name, new_name, line_no)
+ comment = consecutive_comment(line_no)
+ handle_consecutive_comment_directive(@container, comment)
+ visibility = @container.find_method(old_name, @singleton)&.visibility || :public
+ a = RDoc::Alias.new(nil, old_name, new_name, comment, @singleton)
+ a.comment = comment
+ handle_modifier_directive(a, line_no)
+ a.store = @store
+ a.line = line_no
+ record_location(a)
+ if should_document?(a)
+ @container.add_alias(a)
+ @container.find_method(new_name, @singleton)&.visibility = visibility
+ end
+ end
+
+ # Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
+
+ def add_attributes(names, rw, line_no)
+ comment = consecutive_comment(line_no)
+ handle_consecutive_comment_directive(@container, comment)
+ return unless @container.document_children
+
+ names.each do |symbol|
+ a = RDoc::Attr.new(nil, symbol.to_s, rw, comment)
+ a.store = @store
+ a.line = line_no
+ a.singleton = @singleton
+ record_location(a)
+ handle_modifier_directive(a, line_no)
+ @container.add_attribute(a) if should_document?(a)
+ a.visibility = visibility # should set after adding to container
+ end
+ end
+
+ def add_includes_extends(names, rdoc_class, line_no) # :nodoc:
+ comment = consecutive_comment(line_no)
+ handle_consecutive_comment_directive(@container, comment)
+ names.each do |name|
+ ie = @container.add(rdoc_class, name, '')
+ ie.store = @store
+ ie.line = line_no
+ ie.comment = comment
+ record_location(ie)
+ end
+ end
+
+ # Handle `include Foo, Bar`
+
+ def add_includes(names, line_no) # :nodoc:
+ add_includes_extends(names, RDoc::Include, line_no)
+ end
+
+ # Handle `extend Foo, Bar`
+
+ def add_extends(names, line_no) # :nodoc:
+ add_includes_extends(names, RDoc::Extend, line_no)
+ end
+
+ # Adds a method defined by `def` syntax
+
+ def add_method(name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, end_line:)
+ receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
+ meth = RDoc::AnyMethod.new(nil, name)
+ if (comment = consecutive_comment(start_line))
+ handle_consecutive_comment_directive(@container, comment)
+ handle_consecutive_comment_directive(meth, comment)
+
+ comment.normalize
+ comment.extract_call_seq(meth)
+ meth.comment = comment
+ end
+ handle_modifier_directive(meth, start_line)
+ handle_modifier_directive(meth, end_line)
+ return unless should_document?(meth)
+
+
+ if meth.name == 'initialize' && !singleton
+ if meth.dont_rename_initialize
+ visibility = :protected
+ else
+ meth.name = 'new'
+ singleton = true
+ visibility = :public
+ end
+ end
+
+ internal_add_method(
+ receiver,
+ meth,
+ line_no: start_line,
+ visibility: visibility,
+ singleton: singleton,
+ params: params,
+ calls_super: calls_super,
+ block_params: block_params,
+ tokens: tokens
+ )
+ end
+
+ private def internal_add_method(container, meth, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc:
+ meth.name ||= meth.call_seq[/\A[^()\s]+/] if meth.call_seq
+ meth.name ||= 'unknown'
+ meth.store = @store
+ meth.line = line_no
+ meth.singleton = singleton
+ container.add_method(meth) # should add after setting singleton and before setting visibility
+ meth.visibility = visibility
+ meth.params ||= params
+ meth.calls_super = calls_super
+ meth.block_params ||= block_params if block_params
+ record_location(meth)
+ meth.start_collecting_tokens
+ tokens.each do |token|
+ meth.token_stream << token
+ end
+ end
+
+ # Find or create module or class from a given module name.
+ # If module or class does not exist, creates a module or a class according to `create_mode` argument.
+
+ def find_or_create_module_path(module_name, create_mode)
+ root_name, *path, name = module_name.split('::')
+ add_module = ->(mod, name, mode) {
+ case mode
+ when :class
+ mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store }
+ when :module
+ mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store }
+ end
+ }
+ if root_name.empty?
+ mod = @top_level
+ else
+ @module_nesting.reverse_each do |nesting|
+ mod = nesting.find_module_named(root_name)
+ break if mod
+ end
+ return mod || add_module.call(@top_level, root_name, create_mode) unless name
+ mod ||= add_module.call(@top_level, root_name, :module)
+ end
+ path.each do |name|
+ mod = mod.find_module_named(name) || add_module.call(mod, name, :module)
+ end
+ mod.find_module_named(name) || add_module.call(mod, name, create_mode)
+ end
+
+ # Resolves constant path to a full path by searching module nesting
+
+ def resolve_constant_path(constant_path)
+ owner_name, path = constant_path.split('::', 2)
+ return constant_path if owner_name.empty? # ::Foo, ::Foo::Bar
+ mod = nil
+ @module_nesting.reverse_each do |nesting|
+ mod = nesting.find_module_named(owner_name)
+ break if mod
+ end
+ mod ||= @top_level.find_module_named(owner_name)
+ [mod.full_name, path].compact.join('::') if mod
+ end
+
+ # Returns a pair of owner module and constant name from a given constant path.
+ # Creates owner module if it does not exist.
+
+ def find_or_create_constant_owner_name(constant_path)
+ const_path, colon, name = constant_path.rpartition('::')
+ if colon.empty? # class Foo
+ [@container, name]
+ elsif const_path.empty? # class ::Foo
+ [@top_level, name]
+ else # `class Foo::Bar` or `class ::Foo::Bar`
+ [find_or_create_module_path(const_path, :module), name]
+ end
+ end
+
+ # Adds a constant
+
+ def add_constant(constant_name, rhs_name, start_line, end_line)
+ comment = consecutive_comment(start_line)
+ handle_consecutive_comment_directive(@container, comment)
+ owner, name = find_or_create_constant_owner_name(constant_name)
+ constant = RDoc::Constant.new(name, rhs_name, comment)
+ constant.store = @store
+ constant.line = start_line
+ record_location(constant)
+ handle_modifier_directive(constant, start_line)
+ handle_modifier_directive(constant, end_line)
+ owner.add_constant(constant)
+ mod =
+ if rhs_name =~ /^::/
+ @store.find_class_or_module(rhs_name)
+ else
+ @container.find_module_named(rhs_name)
+ end
+ if mod && constant.document_self
+ a = @container.add_module_alias(mod, rhs_name, constant, @top_level)
+ a.store = @store
+ a.line = start_line
+ record_location(a)
+ end
+ end
+
+ # Adds module or class
+
+ def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil)
+ comment = consecutive_comment(start_line)
+ handle_consecutive_comment_directive(@container, comment)
+ return unless @container.document_children
+
+ owner, name = find_or_create_constant_owner_name(module_name)
+ if is_class
+ mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')
+
+ # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information.
+ # We need to fix it when RDoc::NormalClass resolved to a wrong constant name
+ if superclass_name
+ superclass_full_path = resolve_constant_path(superclass_name)
+ superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path
+ superclass_full_path ||= superclass_name
+ if superclass
+ mod.superclass = superclass
+ elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path
+ mod.superclass = superclass_full_path
+ end
+ end
+ else
+ mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name)
+ end
+
+ mod.store = @store
+ mod.line = start_line
+ record_location(mod)
+ handle_modifier_directive(mod, start_line)
+ handle_modifier_directive(mod, end_line)
+ mod.add_comment(comment, @top_level) if comment
+ mod
+ end
+
+ class RDocVisitor < Prism::Visitor # :nodoc:
+ def initialize(scanner, top_level, store)
+ @scanner = scanner
+ @top_level = top_level
+ @store = store
+ end
+
+ def visit_call_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ if node.receiver.nil?
+ case node.name
+ when :attr
+ _visit_call_attr_reader_writer_accessor(node, 'R')
+ when :attr_reader
+ _visit_call_attr_reader_writer_accessor(node, 'R')
+ when :attr_writer
+ _visit_call_attr_reader_writer_accessor(node, 'W')
+ when :attr_accessor
+ _visit_call_attr_reader_writer_accessor(node, 'RW')
+ when :include
+ _visit_call_include(node)
+ when :extend
+ _visit_call_extend(node)
+ when :public
+ _visit_call_public_private_protected(node, :public) { super }
+ when :private
+ _visit_call_public_private_protected(node, :private) { super }
+ when :protected
+ _visit_call_public_private_protected(node, :protected) { super }
+ when :private_constant
+ _visit_call_private_constant(node)
+ when :public_constant
+ _visit_call_public_constant(node)
+ when :require
+ _visit_call_require(node)
+ when :alias_method
+ _visit_call_alias_method(node)
+ when :module_function
+ _visit_call_module_function(node) { super }
+ when :public_class_method
+ _visit_call_public_private_class_method(node, :public) { super }
+ when :private_class_method
+ _visit_call_public_private_class_method(node, :private) { super }
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+
+ def visit_alias_method_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ return unless node.old_name.is_a?(Prism::SymbolNode) && node.new_name.is_a?(Prism::SymbolNode)
+ @scanner.add_alias_method(node.old_name.value.to_s, node.new_name.value.to_s, node.location.start_line)
+ end
+
+ def visit_module_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ module_name = constant_path_string(node.constant_path)
+ mod = @scanner.add_module_or_class(module_name, node.location.start_line, node.location.end_line) if module_name
+ if mod
+ @scanner.with_container(mod) do
+ super
+ @scanner.process_comments_until(node.location.end_line)
+ end
+ else
+ @scanner.skip_comments_until(node.location.end_line)
+ end
+ end
+
+ def visit_class_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ superclass_name = constant_path_string(node.superclass) if node.superclass
+ class_name = constant_path_string(node.constant_path)
+ klass = @scanner.add_module_or_class(class_name, node.location.start_line, node.location.end_line, is_class: true, superclass_name: superclass_name) if class_name
+ if klass
+ @scanner.with_container(klass) do
+ super
+ @scanner.process_comments_until(node.location.end_line)
+ end
+ else
+ @scanner.skip_comments_until(node.location.end_line)
+ end
+ end
+
+ def visit_singleton_class_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+
+ expression = node.expression
+ expression = expression.body.body.first if expression.is_a?(Prism::ParenthesesNode) && expression.body&.body&.size == 1
+
+ case expression
+ when Prism::ConstantWriteNode
+ # Accept `class << (NameErrorCheckers = Object.new)` as a module which is not actually a module
+ mod = @scanner.container.add_module(RDoc::NormalModule, expression.name.to_s)
+ when Prism::ConstantPathNode, Prism::ConstantReadNode
+ expression_name = constant_path_string(expression)
+ # If a constant_path does not exist, RDoc creates a module
+ mod = @scanner.find_or_create_module_path(expression_name, :module) if expression_name
+ when Prism::SelfNode
+ mod = @scanner.container if @scanner.container != @top_level
+ end
+ if mod
+ @scanner.with_container(mod, singleton: true) do
+ super
+ @scanner.process_comments_until(node.location.end_line)
+ end
+ else
+ @scanner.skip_comments_until(node.location.end_line)
+ end
+ end
+
+ def visit_def_node(node)
+ start_line = node.location.start_line
+ end_line = node.location.end_line
+ @scanner.process_comments_until(start_line - 1)
+
+ case node.receiver
+ when Prism::NilNode, Prism::TrueNode, Prism::FalseNode
+ visibility = :public
+ singleton = false
+ receiver_name =
+ case node.receiver
+ when Prism::NilNode
+ 'NilClass'
+ when Prism::TrueNode
+ 'TrueClass'
+ when Prism::FalseNode
+ 'FalseClass'
+ end
+ receiver_fallback_type = :class
+ when Prism::SelfNode
+ # singleton method of a singleton class is not documentable
+ return if @scanner.singleton
+ visibility = :public
+ singleton = true
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
+ visibility = :public
+ singleton = true
+ receiver_name = constant_path_string(node.receiver)
+ receiver_fallback_type = :module
+ return unless receiver_name
+ when nil
+ visibility = @scanner.visibility
+ singleton = @scanner.singleton
+ else
+ # `def (unknown expression).method_name` is not documentable
+ return
+ end
+ name = node.name.to_s
+ params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node)
+ tokens = @scanner.visible_tokens_from_location(node.location)
+
+ @scanner.add_method(
+ name,
+ receiver_name: receiver_name,
+ receiver_fallback_type: receiver_fallback_type,
+ visibility: visibility,
+ singleton: singleton,
+ params: params,
+ block_params: block_params,
+ calls_super: calls_super,
+ tokens: tokens,
+ start_line: start_line,
+ end_line: end_line
+ )
+ ensure
+ @scanner.skip_comments_until(end_line)
+ end
+
+ def visit_constant_path_write_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ path = constant_path_string(node.target)
+ return unless path
+
+ @scanner.add_constant(
+ path,
+ constant_path_string(node.value) || node.value.slice,
+ node.location.start_line,
+ node.location.end_line
+ )
+ @scanner.skip_comments_until(node.location.end_line)
+ # Do not traverse rhs not to document `A::B = Struct.new{def undocumentable_method; end}`
+ end
+
+ def visit_constant_write_node(node)
+ @scanner.process_comments_until(node.location.start_line - 1)
+ @scanner.add_constant(
+ node.name.to_s,
+ constant_path_string(node.value) || node.value.slice,
+ node.location.start_line,
+ node.location.end_line
+ )
+ @scanner.skip_comments_until(node.location.end_line)
+ # Do not traverse rhs not to document `A = Struct.new{def undocumentable_method; end}`
+ end
+
+ private
+
+ def constant_arguments_names(call_node)
+ return unless call_node.arguments
+ names = call_node.arguments.arguments.map { |arg| constant_path_string(arg) }
+ names.all? ? names : nil
+ end
+
+ def symbol_arguments(call_node)
+ arguments_node = call_node.arguments
+ return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)}
+ arguments_node.arguments.map { |arg| arg.value.to_sym }
+ end
+
+ def visibility_method_arguments(call_node, singleton:)
+ arguments_node = call_node.arguments
+ return unless arguments_node
+ symbols = symbol_arguments(call_node)
+ if symbols
+ # module_function :foo, :bar
+ return symbols.map(&:to_s)
+ else
+ return unless arguments_node.arguments.size == 1
+ arg = arguments_node.arguments.first
+ return unless arg.is_a?(Prism::DefNode)
+
+ if singleton
+ # `private_class_method def foo; end` `private_class_method def not_self.foo; end` should be ignored
+ return unless arg.receiver.is_a?(Prism::SelfNode)
+ else
+ # `module_function def something.foo` should be ignored
+ return if arg.receiver
+ end
+ # `module_function def foo; end` or `private_class_method def self.foo; end`
+ [arg.name.to_s]
+ end
+ end
+
+ def constant_path_string(node)
+ case node
+ when Prism::ConstantReadNode
+ node.name.to_s
+ when Prism::ConstantPathNode
+ parent_name = node.parent ? constant_path_string(node.parent) : ''
+ "#{parent_name}::#{node.name}" if parent_name
+ end
+ end
+
+ def _visit_call_require(call_node)
+ return unless call_node.arguments&.arguments&.size == 1
+ arg = call_node.arguments.arguments.first
+ return unless arg.is_a?(Prism::StringNode)
+ @scanner.container.add_require(RDoc::Require.new(arg.unescaped, nil))
+ end
+
+ def _visit_call_module_function(call_node)
+ yield
+ return if @scanner.singleton
+ names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s)
+ @scanner.change_method_to_module_function(names) if names
+ end
+
+ def _visit_call_public_private_class_method(call_node, visibility)
+ yield
+ return if @scanner.singleton
+ names = visibility_method_arguments(call_node, singleton: true)
+ @scanner.change_method_visibility(names, visibility, singleton: true) if names
+ end
+
+ def _visit_call_public_private_protected(call_node, visibility)
+ arguments_node = call_node.arguments
+ if arguments_node.nil? # `public` `private`
+ @scanner.visibility = visibility
+ else # `public :foo, :bar`, `private def foo; end`
+ yield
+ names = visibility_method_arguments(call_node, singleton: @scanner.singleton)
+ @scanner.change_method_visibility(names, visibility) if names
+ end
+ end
+
+ def _visit_call_alias_method(call_node)
+ new_name, old_name, *rest = symbol_arguments(call_node)
+ return unless old_name && new_name && rest.empty?
+ @scanner.add_alias_method(old_name.to_s, new_name.to_s, call_node.location.start_line)
+ end
+
+ def _visit_call_include(call_node)
+ names = constant_arguments_names(call_node)
+ line_no = call_node.location.start_line
+ return unless names
+
+ if @scanner.singleton
+ @scanner.add_extends(names, line_no)
+ else
+ @scanner.add_includes(names, line_no)
+ end
+ end
+
+ def _visit_call_extend(call_node)
+ names = constant_arguments_names(call_node)
+ @scanner.add_extends(names, call_node.location.start_line) if names && !@scanner.singleton
+ end
+
+ def _visit_call_public_constant(call_node)
+ return if @scanner.singleton
+ names = symbol_arguments(call_node)
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :public) if names
+ end
+
+ def _visit_call_private_constant(call_node)
+ return if @scanner.singleton
+ names = symbol_arguments(call_node)
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :private) if names
+ end
+
+ def _visit_call_attr_reader_writer_accessor(call_node, rw)
+ names = symbol_arguments(call_node)
+ @scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names
+ end
+ class MethodSignatureVisitor < Prism::Visitor # :nodoc:
+ class << self
+ def scan_signature(def_node)
+ visitor = new
+ def_node.body&.accept(visitor)
+ params = "(#{def_node.parameters&.slice})"
+ block_params = visitor.yields.first
+ [params, block_params, visitor.calls_super]
+ end
+ end
+
+ attr_reader :params, :yields, :calls_super
+
+ def initialize
+ @params = nil
+ @calls_super = false
+ @yields = []
+ end
+
+ def visit_def_node(node)
+ # stop traverse inside nested def
+ end
+
+ def visit_yield_node(node)
+ @yields << (node.arguments&.slice || '')
+ end
+
+ def visit_super_node(node)
+ @calls_super = true
+ super
+ end
+
+ def visit_forwarding_super_node(node)
+ @calls_super = true
+ end
+ end
+ end
+end
diff --git a/lib/rdoc/parser/ripper_state_lex.rb b/lib/rdoc/parser/ripper_state_lex.rb
index f6cefd0305..2212906bbd 100644
--- a/lib/rdoc/parser/ripper_state_lex.rb
+++ b/lib/rdoc/parser/ripper_state_lex.rb
@@ -7,307 +7,12 @@ require 'ripper'
class RDoc::Parser::RipperStateLex
# :stopdoc:
- # TODO: Remove this constants after Ruby 2.4 EOL
- RIPPER_HAS_LEX_STATE = Ripper::Filter.method_defined?(:state)
-
Token = Struct.new(:line_no, :char_no, :kind, :text, :state)
- EXPR_NONE = 0
- EXPR_BEG = 1
- EXPR_END = 2
- EXPR_ENDARG = 4
- EXPR_ENDFN = 8
- EXPR_ARG = 16
- EXPR_CMDARG = 32
- EXPR_MID = 64
- EXPR_FNAME = 128
- EXPR_DOT = 256
- EXPR_CLASS = 512
- EXPR_LABEL = 1024
- EXPR_LABELED = 2048
- EXPR_FITEM = 4096
- EXPR_VALUE = EXPR_BEG
- EXPR_BEG_ANY = (EXPR_BEG | EXPR_MID | EXPR_CLASS)
- EXPR_ARG_ANY = (EXPR_ARG | EXPR_CMDARG)
- EXPR_END_ANY = (EXPR_END | EXPR_ENDARG | EXPR_ENDFN)
-
- class InnerStateLex < Ripper::Filter
- attr_accessor :lex_state
-
- def initialize(code)
- @lex_state = EXPR_BEG
- @in_fname = false
- @continue = false
- reset
- super(code)
- end
-
- def reset
- @command_start = false
- @cmd_state = @command_start
- end
-
- def on_nl(tok, data)
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @continue = true
- else
- @continue = false
- @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
- end
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_ignored_nl(tok, data)
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @continue = true
- else
- @continue = false
- @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
- end
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_op(tok, data)
- case tok
- when '&', '|', '!', '!=', '!~'
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- when '<<'
- # TODO next token?
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- when '?'
- @lex_state = EXPR_BEG
- when '&&', '||', '+=', '-=', '*=', '**=',
- '&=', '|=', '^=', '<<=', '>>=', '||=', '&&='
- @lex_state = EXPR_BEG
- when '::'
- case @lex_state
- when EXPR_ARG, EXPR_CMDARG
- @lex_state = EXPR_DOT
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- else
- case @lex_state
- when EXPR_FNAME, EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_BEG
- end
- end
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_kw(tok, data)
- case tok
- when 'class'
- @lex_state = EXPR_CLASS
- @in_fname = true
- when 'def'
- @lex_state = EXPR_FNAME
- @continue = true
- @in_fname = true
- when 'if', 'unless', 'while', 'until'
- if ((EXPR_MID | EXPR_END | EXPR_ENDARG | EXPR_ENDFN | EXPR_ARG | EXPR_CMDARG) & @lex_state) != 0 # postfix if
- @lex_state = EXPR_BEG | EXPR_LABEL
- else
- @lex_state = EXPR_BEG
- end
- when 'begin', 'case', 'when'
- @lex_state = EXPR_BEG
- when 'return', 'break'
- @lex_state = EXPR_MID
- else
- if @lex_state == EXPR_FNAME
- @lex_state = EXPR_END
- else
- @lex_state = EXPR_END
- end
- end
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_tstring_beg(tok, data)
- @lex_state = EXPR_BEG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_tstring_end(tok, data)
- @lex_state = EXPR_END | EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_CHAR(tok, data)
- @lex_state = EXPR_END
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_period(tok, data)
- @lex_state = EXPR_DOT
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_int(tok, data)
- @lex_state = EXPR_END | EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_float(tok, data)
- @lex_state = EXPR_END | EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_rational(tok, data)
- @lex_state = EXPR_END | EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_imaginary(tok, data)
- @lex_state = EXPR_END | EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_symbeg(tok, data)
- @lex_state = EXPR_FNAME
- @continue = true
- @in_fname = true
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- private def on_variables(event, tok, data)
- if @in_fname
- @lex_state = EXPR_ENDFN
- @in_fname = false
- @continue = false
- elsif @continue
- case @lex_state
- when EXPR_DOT
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_ENDFN
- @continue = false
- end
- else
- @lex_state = EXPR_CMDARG
- end
- data << Token.new(lineno, column, event, tok, @lex_state)
- end
-
- def on_ident(tok, data)
- on_variables(__method__, tok, data)
- end
-
- def on_ivar(tok, data)
- @lex_state = EXPR_END
- on_variables(__method__, tok, data)
- end
-
- def on_cvar(tok, data)
- @lex_state = EXPR_END
- on_variables(__method__, tok, data)
- end
-
- def on_gvar(tok, data)
- @lex_state = EXPR_END
- on_variables(__method__, tok, data)
- end
-
- def on_backref(tok, data)
- @lex_state = EXPR_END
- on_variables(__method__, tok, data)
- end
-
- def on_lparen(tok, data)
- @lex_state = EXPR_LABEL | EXPR_BEG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_rparen(tok, data)
- @lex_state = EXPR_ENDFN
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_lbrace(tok, data)
- @lex_state = EXPR_LABEL | EXPR_BEG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_rbrace(tok, data)
- @lex_state = EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_lbracket(tok, data)
- @lex_state = EXPR_LABEL | EXPR_BEG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_rbracket(tok, data)
- @lex_state = EXPR_ENDARG
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_const(tok, data)
- case @lex_state
- when EXPR_FNAME
- @lex_state = EXPR_ENDFN
- when EXPR_CLASS, EXPR_CMDARG, EXPR_MID
- @lex_state = EXPR_ARG
- else
- @lex_state = EXPR_CMDARG
- end
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_sp(tok, data)
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_comma(tok, data)
- @lex_state = EXPR_BEG | EXPR_LABEL if (EXPR_ARG_ANY & @lex_state) != 0
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_comment(tok, data)
- @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_ignored_sp(tok, data)
- @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- end
-
- def on_heredoc_beg(tok, data)
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- @lex_state = EXPR_END
- data
- end
-
- def on_heredoc_end(tok, data)
- data << Token.new(lineno, column, __method__, tok, @lex_state)
- @lex_state = EXPR_BEG
- data
- end
-
- def on_default(event, tok, data)
- reset
- data << Token.new(lineno, column, event, tok, @lex_state)
- end
- end unless RIPPER_HAS_LEX_STATE
+ EXPR_END = Ripper::EXPR_END
+ EXPR_ENDFN = Ripper::EXPR_ENDFN
+ EXPR_ARG = Ripper::EXPR_ARG
+ EXPR_FNAME = Ripper::EXPR_FNAME
class InnerStateLex < Ripper::Filter
def initialize(code)
@@ -317,7 +22,7 @@ class RDoc::Parser::RipperStateLex
def on_default(event, tok, data)
data << Token.new(lineno, column, event, tok, state)
end
- end if RIPPER_HAS_LEX_STATE
+ end
def get_squashed_tk
if @buf.empty?
@@ -333,9 +38,8 @@ class RDoc::Parser::RipperStateLex
tk = get_string_tk(tk)
when :on_backtick then
if (tk[:state] & (EXPR_FNAME | EXPR_ENDFN)) != 0
- @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
tk[:kind] = :on_ident
- tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
+ tk[:state] = Ripper::Lexer::State.new(EXPR_ARG)
else
tk = get_string_tk(tk)
end
@@ -345,7 +49,6 @@ class RDoc::Parser::RipperStateLex
tk = get_embdoc_tk(tk)
when :on_heredoc_beg then
@heredoc_queue << retrieve_heredoc_info(tk)
- @inner_lex.lex_state = EXPR_END unless RIPPER_HAS_LEX_STATE
when :on_nl, :on_ignored_nl, :on_comment, :on_heredoc_end then
if !@heredoc_queue.empty?
get_heredoc_tk(*@heredoc_queue.shift)
@@ -549,8 +252,7 @@ class RDoc::Parser::RipperStateLex
private def get_op_tk(tk)
redefinable_operators = %w[! != !~ % & * ** + +@ - -@ / < << <= <=> == === =~ > >= >> [] []= ^ ` | ~]
if redefinable_operators.include?(tk[:text]) and tk[:state] == EXPR_ARG then
- @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
- tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
+ tk[:state] = Ripper::Lexer::State.new(EXPR_ARG)
tk[:kind] = :on_ident
elsif tk[:text] =~ /^[-+]$/ then
tk_ahead = get_squashed_tk
diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb
index 85f1cd0391..909b02d2af 100644
--- a/lib/rdoc/parser/ruby.rb
+++ b/lib/rdoc/parser/ruby.rb
@@ -8,6 +8,15 @@
# by Keiju ISHITSUKA (Nippon Rational Inc.)
#
+if ENV['RDOC_USE_PRISM_PARSER']
+ require 'rdoc/parser/prism_ruby'
+ RDoc::Parser.const_set(:Ruby, RDoc::Parser::PrismRuby)
+ puts "========================================================================="
+ puts "RDoc is using the experimental Prism parser to generate the documentation"
+ puts "========================================================================="
+ return
+end
+
require 'ripper'
require_relative 'ripper_state_lex'
@@ -513,7 +522,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
when :on_comment, :on_embdoc then
@read.pop
if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and
- (!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then
+ (!continue or (tk[:state] & Ripper::EXPR_LABEL) != 0) then
break if !continue and nest <= 0
end
when :on_comma then
@@ -526,7 +535,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
nest += 1
when 'if', 'unless', 'while', 'until', 'rescue'
# postfix if/unless/while/until/rescue must be EXPR_LABEL
- nest += 1 unless (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0
+ nest += 1 unless (tk[:state] & Ripper::EXPR_LABEL) != 0
when 'end'
nest -= 1
break if nest == 0
@@ -1041,7 +1050,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
elsif (:on_kw == tk[:kind] && 'def' == tk[:text]) then
nest += 1
elsif (:on_kw == tk[:kind] && %w{do if unless case begin}.include?(tk[:text])) then
- if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
+ if (tk[:state] & Ripper::EXPR_LABEL) == 0
nest += 1
end
elsif [:on_rparen, :on_rbrace, :on_rbracket].include?(tk[:kind]) ||
@@ -1662,7 +1671,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
when :on_comment, :on_embdoc then
@read.pop
if :on_nl == end_token[:kind] and "\n" == tk[:text][-1] and
- (!continue or (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) != 0) then
+ (!continue or (tk[:state] & Ripper::EXPR_LABEL) != 0) then
if method && method.block_params.nil? then
unget_tk tk
read_documentation_modifiers method, modifiers
@@ -1882,7 +1891,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
end
when 'until', 'while' then
- if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
+ if (tk[:state] & Ripper::EXPR_LABEL) == 0
nest += 1
skip_optional_do_after_expression
end
@@ -1898,7 +1907,7 @@ class RDoc::Parser::Ruby < RDoc::Parser
skip_optional_do_after_expression
when 'case', 'do', 'if', 'unless', 'begin' then
- if (tk[:state] & RDoc::Parser::RipperStateLex::EXPR_LABEL) == 0
+ if (tk[:state] & Ripper::EXPR_LABEL) == 0
nest += 1
end
diff --git a/lib/rdoc/rd/block_parser.rb b/lib/rdoc/rd/block_parser.rb
index 527147d91d..256ba553e5 100644
--- a/lib/rdoc/rd/block_parser.rb
+++ b/lib/rdoc/rd/block_parser.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
# DO NOT MODIFY!!!!
-# This file is automatically generated by Racc 1.7.3
+# This file is automatically generated by Racc 1.8.1
# from Racc grammar file "block_parser.ry".
#
@@ -23,7 +23,7 @@ unless $".find {|p| p.end_with?('/racc/info.rb')}
$".push "#{__dir__}/racc/info.rb"
module Racc
- VERSION = '1.7.3'
+ VERSION = '1.8.1'
Version = VERSION
Copyright = 'Copyright (c) 1999-2006 Minero Aoki'
end
@@ -31,10 +31,6 @@ end
end
-unless defined?(NotImplementedError)
- NotImplementedError = NotImplementError # :nodoc:
-end
-
module Racc
class ParseError < StandardError; end
end
@@ -42,7 +38,7 @@ unless defined?(::ParseError)
ParseError = Racc::ParseError # :nodoc:
end
-# Racc is a LALR(1) parser generator.
+# Racc is an LALR(1) parser generator.
# It is written in Ruby itself, and generates Ruby programs.
#
# == Command-line Reference
diff --git a/lib/rdoc/rd/inline_parser.rb b/lib/rdoc/rd/inline_parser.rb
index adacf64d5b..b6d521c6bd 100644
--- a/lib/rdoc/rd/inline_parser.rb
+++ b/lib/rdoc/rd/inline_parser.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
# DO NOT MODIFY!!!!
-# This file is automatically generated by Racc 1.7.3
+# This file is automatically generated by Racc 1.8.1
# from Racc grammar file "inline_parser.ry".
#
@@ -23,7 +23,7 @@ unless $".find {|p| p.end_with?('/racc/info.rb')}
$".push "#{__dir__}/racc/info.rb"
module Racc
- VERSION = '1.7.3'
+ VERSION = '1.8.1'
Version = VERSION
Copyright = 'Copyright (c) 1999-2006 Minero Aoki'
end
@@ -31,10 +31,6 @@ end
end
-unless defined?(NotImplementedError)
- NotImplementedError = NotImplementError # :nodoc:
-end
-
module Racc
class ParseError < StandardError; end
end
@@ -42,7 +38,7 @@ unless defined?(::ParseError)
ParseError = Racc::ParseError # :nodoc:
end
-# Racc is a LALR(1) parser generator.
+# Racc is an LALR(1) parser generator.
# It is written in Ruby itself, and generates Ruby programs.
#
# == Command-line Reference
diff --git a/lib/rdoc/rdoc.gemspec b/lib/rdoc/rdoc.gemspec
index 93a281c8ae..26f9ba1a87 100644
--- a/lib/rdoc/rdoc.gemspec
+++ b/lib/rdoc/rdoc.gemspec
@@ -46,27 +46,27 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"LEGAL.rdoc",
"LICENSE.rdoc",
"README.rdoc",
- "RI.rdoc",
+ "RI.md",
"TODO.rdoc",
"exe/rdoc",
"exe/ri",
"lib/rdoc.rb",
- "lib/rdoc/alias.rb",
- "lib/rdoc/anon_class.rb",
- "lib/rdoc/any_method.rb",
- "lib/rdoc/attr.rb",
- "lib/rdoc/class_module.rb",
+ "lib/rdoc/code_object/alias.rb",
+ "lib/rdoc/code_object/anon_class.rb",
+ "lib/rdoc/code_object/any_method.rb",
+ "lib/rdoc/code_object/attr.rb",
+ "lib/rdoc/code_object/class_module.rb",
"lib/rdoc/code_object.rb",
"lib/rdoc/code_objects.rb",
"lib/rdoc/comment.rb",
- "lib/rdoc/constant.rb",
- "lib/rdoc/context.rb",
- "lib/rdoc/context/section.rb",
+ "lib/rdoc/code_object/constant.rb",
+ "lib/rdoc/code_object/context.rb",
+ "lib/rdoc/code_object/context/section.rb",
"lib/rdoc/cross_reference.rb",
"lib/rdoc/encoding.rb",
"lib/rdoc/erb_partial.rb",
"lib/rdoc/erbio.rb",
- "lib/rdoc/extend.rb",
+ "lib/rdoc/code_object/extend.rb",
"lib/rdoc/generator.rb",
"lib/rdoc/generator/darkfish.rb",
"lib/rdoc/generator/json_index.rb",
@@ -136,11 +136,11 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"lib/rdoc/generator/template/json_index/.document",
"lib/rdoc/generator/template/json_index/js/navigation.js",
"lib/rdoc/generator/template/json_index/js/searcher.js",
- "lib/rdoc/ghost_method.rb",
+ "lib/rdoc/code_object/ghost_method.rb",
"lib/rdoc/i18n.rb",
"lib/rdoc/i18n/locale.rb",
"lib/rdoc/i18n/text.rb",
- "lib/rdoc/include.rb",
+ "lib/rdoc/code_object/include.rb",
"lib/rdoc/known_classes.rb",
"lib/rdoc/markdown.kpeg",
"lib/rdoc/markdown/entities.rb",
@@ -180,11 +180,11 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"lib/rdoc/markup/to_test.rb",
"lib/rdoc/markup/to_tt_only.rb",
"lib/rdoc/markup/verbatim.rb",
- "lib/rdoc/meta_method.rb",
- "lib/rdoc/method_attr.rb",
- "lib/rdoc/mixin.rb",
- "lib/rdoc/normal_class.rb",
- "lib/rdoc/normal_module.rb",
+ "lib/rdoc/code_object/meta_method.rb",
+ "lib/rdoc/code_object/method_attr.rb",
+ "lib/rdoc/code_object/mixin.rb",
+ "lib/rdoc/code_object/normal_class.rb",
+ "lib/rdoc/code_object/normal_module.rb",
"lib/rdoc/options.rb",
"lib/rdoc/parser.rb",
"lib/rdoc/parser/c.rb",
@@ -201,7 +201,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"lib/rdoc/rd/inline.rb",
"lib/rdoc/rd/inline_parser.ry",
"lib/rdoc/rdoc.rb",
- "lib/rdoc/require.rb",
+ "lib/rdoc/code_object/require.rb",
"lib/rdoc/ri.rb",
"lib/rdoc/ri/driver.rb",
"lib/rdoc/ri/formatter.rb",
@@ -210,7 +210,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"lib/rdoc/ri/task.rb",
"lib/rdoc/rubygems_hook.rb",
"lib/rdoc/servlet.rb",
- "lib/rdoc/single_class.rb",
+ "lib/rdoc/code_object/single_class.rb",
"lib/rdoc/stats.rb",
"lib/rdoc/stats/normal.rb",
"lib/rdoc/stats/quiet.rb",
@@ -220,7 +220,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
"lib/rdoc/text.rb",
"lib/rdoc/token_stream.rb",
"lib/rdoc/tom_doc.rb",
- "lib/rdoc/top_level.rb",
+ "lib/rdoc/code_object/top_level.rb",
"lib/rdoc/version.rb",
"man/ri.1",
]
diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb
index 2da6d9b575..47108ceee3 100644
--- a/lib/rdoc/rdoc.rb
+++ b/lib/rdoc/rdoc.rb
@@ -356,7 +356,7 @@ option)
top_level = @store.add_file filename, relative_name: relative_path.to_s
- parser = RDoc::Parser.for top_level, filename, content, @options, @stats
+ parser = RDoc::Parser.for top_level, content, @options, @stats
return unless parser
@@ -544,7 +544,7 @@ end
begin
require 'rubygems'
- rdoc_extensions = Gem.find_files 'rdoc/discover'
+ rdoc_extensions = Gem.find_latest_files 'rdoc/discover'
rdoc_extensions.each do |extension|
begin
diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb
index 64783dc163..c6fddbac67 100644
--- a/lib/rdoc/ri/driver.rb
+++ b/lib/rdoc/ri/driver.rb
@@ -110,10 +110,6 @@ class RDoc::RI::Driver
options = default_options
opts = OptionParser.new do |opt|
- opt.accept File do |file,|
- File.readable?(file) and not File.directory?(file) and file
- end
-
opt.program_name = File.basename $0
opt.version = RDoc::VERSION
opt.release = nil
@@ -345,9 +341,17 @@ or the PAGER environment variable.
opt.separator nil
- opt.on("--dump=CACHE", File,
+ opt.on("--dump=CACHE",
"Dump data from an ri cache or data file.") do |value|
- options[:dump_path] = value
+ unless File.readable?(value)
+ abort "#{value.inspect} is not readable"
+ end
+
+ if File.directory?(value)
+ abort "#{value.inspect} is a directory"
+ end
+
+ options[:dump_path] = File.new(value)
end
end
diff --git a/lib/rdoc/task.rb b/lib/rdoc/task.rb
index eb584c9d2a..ba697d0a93 100644
--- a/lib/rdoc/task.rb
+++ b/lib/rdoc/task.rb
@@ -104,9 +104,8 @@ class RDoc::Task < Rake::TaskLib
attr_accessor :name
##
- # Comment markup format. rdoc, rd and tomdoc are supported. (default is
- # 'rdoc')
-
+ # The markup format; one of: +rdoc+ (the default), +markdown+, +rd+, +tomdoc+.
+ # See {Markup Formats}[rdoc-ref:RDoc::Markup@Markup+Formats].
attr_accessor :markup
##
diff --git a/lib/rdoc/tom_doc.rb b/lib/rdoc/tom_doc.rb
index e161fcf42f..d10f024f70 100644
--- a/lib/rdoc/tom_doc.rb
+++ b/lib/rdoc/tom_doc.rb
@@ -3,13 +3,7 @@
# A parser for TomDoc based on TomDoc 1.0.0-rc1 (02adef9b5a)
#
-# The TomDoc specification can be found at:
-#
-# http://tomdoc.org
-#
-# The latest version of the TomDoc specification can be found at:
-#
-# https://github.com/mojombo/tomdoc/blob/master/tomdoc.md
+# The TomDoc specification can be found at http://tomdoc.org.
#
# To choose TomDoc as your only default format see RDoc::Options@Saved+Options
# for instructions on setting up a <code>.rdoc_options</code> file to store
diff --git a/lib/rdoc/version.rb b/lib/rdoc/version.rb
index 87842d9847..427d4ae232 100644
--- a/lib/rdoc/version.rb
+++ b/lib/rdoc/version.rb
@@ -5,6 +5,6 @@ module RDoc
##
# RDoc version you are using
- VERSION = '6.6.3.1'
+ VERSION = '6.7.0'
end
diff --git a/lib/reline.rb b/lib/reline.rb
index fb00b96531..ddb0224180 100644
--- a/lib/reline.rb
+++ b/lib/reline.rb
@@ -7,6 +7,7 @@ require 'reline/key_stroke'
require 'reline/line_editor'
require 'reline/history'
require 'reline/terminfo'
+require 'reline/io'
require 'reline/face'
require 'rbconfig'
@@ -18,20 +19,10 @@ module Reline
class ConfigEncodingConversionError < StandardError; end
Key = Struct.new(:char, :combined_char, :with_meta) do
- def match?(other)
- case other
- when Reline::Key
- (other.char.nil? or char.nil? or char == other.char) and
- (other.combined_char.nil? or combined_char.nil? or combined_char == other.combined_char) and
- (other.with_meta.nil? or with_meta.nil? or with_meta == other.with_meta)
- when Integer, Symbol
- (combined_char and combined_char == other) or
- (combined_char.nil? and char and char == other)
- else
- false
- end
+ # For dialog_proc `key.match?(dialog.name)`
+ def match?(sym)
+ combined_char.is_a?(Symbol) && combined_char == sym
end
- alias_method :==, :match?
end
CursorPos = Struct.new(:x, :y)
DialogRenderInfo = Struct.new(
@@ -263,7 +254,6 @@ module Reline
raise ArgumentError.new('#readmultiline needs block to confirm multiline termination')
end
- Reline.update_iogate
io_gate.with_raw_input do
inner_readline(prompt, add_hist, true, &confirm_multiline_termination)
end
@@ -286,7 +276,6 @@ module Reline
def readline(prompt = '', add_hist = false)
@mutex.synchronize do
- Reline.update_iogate
io_gate.with_raw_input do
inner_readline(prompt, add_hist, false)
end
@@ -335,14 +324,17 @@ module Reline
line_editor.prompt_proc = prompt_proc
line_editor.auto_indent_proc = auto_indent_proc
line_editor.dig_perfect_match_proc = dig_perfect_match_proc
+
+ # Readline calls pre_input_hook just after printing the first prompt.
+ line_editor.print_nomultiline_prompt
pre_input_hook&.call
- unless Reline::IOGate == Reline::GeneralIO
+
+ unless Reline::IOGate.dumb?
@dialog_proc_list.each_pair do |name_sym, d|
line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context)
end
end
- line_editor.print_nomultiline_prompt(prompt)
line_editor.update_dialogs
line_editor.rerender
@@ -354,7 +346,7 @@ module Reline
inputs.each do |key|
if key.char == :bracketed_paste_start
text = io_gate.read_bracketed_paste
- line_editor.insert_pasted_text(text)
+ line_editor.insert_multiline_text(text)
line_editor.scroll_into_view
else
line_editor.update(key)
@@ -378,92 +370,39 @@ module Reline
end
end
- # GNU Readline waits for "keyseq-timeout" milliseconds to see if the ESC
- # is followed by a character, and times out and treats it as a standalone
- # ESC if the second character does not arrive. If the second character
- # comes before timed out, it is treated as a modifier key with the
- # meta-property of meta-key, so that it can be distinguished from
- # multibyte characters with the 8th bit turned on.
- #
- # GNU Readline will wait for the 2nd character with "keyseq-timeout"
- # milli-seconds but wait forever after 3rd characters.
+ # GNU Readline watis for "keyseq-timeout" milliseconds when the input is
+ # ambiguous whether it is matching or matched.
+ # If the next character does not arrive within the specified timeout, input
+ # is considered as matched.
+ # `ESC` is ambiguous because it can be a standalone ESC (matched) or part of
+ # `ESC char` or part of CSI sequence (matching).
private def read_io(keyseq_timeout, &block)
buffer = []
+ status = KeyStroke::MATCHING
loop do
- c = io_gate.getc(Float::INFINITY)
- if c == -1
- result = :unmatched
- else
- buffer << c
- result = key_stroke.match_status(buffer)
- end
- case result
- when :matched
- expanded = key_stroke.expand(buffer).map{ |expanded_c|
- Reline::Key.new(expanded_c, expanded_c, false)
- }
- block.(expanded)
- break
- when :matching
- if buffer.size == 1
- case read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
- when :break then break
- when :next then next
- end
- end
- when :unmatched
- if buffer.size == 1 and c == "\e".ord
- read_escaped_key(keyseq_timeout, c, block)
+ timeout = status == KeyStroke::MATCHING_MATCHED ? keyseq_timeout.fdiv(1000) : Float::INFINITY
+ c = io_gate.getc(timeout)
+ if c.nil? || c == -1
+ if status == KeyStroke::MATCHING_MATCHED
+ status = KeyStroke::MATCHED
+ elsif buffer.empty?
+ # io_gate is closed and reached EOF
+ block.call([Key.new(nil, nil, false)])
+ return
else
- expanded = buffer.map{ |expanded_c|
- Reline::Key.new(expanded_c, expanded_c, false)
- }
- block.(expanded)
+ status = KeyStroke::UNMATCHED
end
- break
+ else
+ buffer << c
+ status = key_stroke.match_status(buffer)
end
- end
- end
- private def read_2nd_character_of_key_sequence(keyseq_timeout, buffer, c, block)
- succ_c = io_gate.getc(keyseq_timeout.fdiv(1000))
- if succ_c
- case key_stroke.match_status(buffer.dup.push(succ_c))
- when :unmatched
- if c == "\e".ord
- block.([Reline::Key.new(succ_c, succ_c | 0b10000000, true)])
- else
- block.([Reline::Key.new(c, c, false), Reline::Key.new(succ_c, succ_c, false)])
- end
- return :break
- when :matching
- io_gate.ungetc(succ_c)
- return :next
- when :matched
- buffer << succ_c
- expanded = key_stroke.expand(buffer).map{ |expanded_c|
- Reline::Key.new(expanded_c, expanded_c, false)
- }
- block.(expanded)
- return :break
+ if status == KeyStroke::MATCHED || status == KeyStroke::UNMATCHED
+ expanded, rest_bytes = key_stroke.expand(buffer)
+ rest_bytes.reverse_each { |c| io_gate.ungetc(c) }
+ block.call(expanded)
+ return
end
- else
- block.([Reline::Key.new(c, c, false)])
- return :break
- end
- end
-
- private def read_escaped_key(keyseq_timeout, c, block)
- escaped_c = io_gate.getc(keyseq_timeout.fdiv(1000))
-
- if escaped_c.nil?
- block.([Reline::Key.new(c, c, false)])
- elsif escaped_c >= 128 # maybe, first byte of multi byte
- block.([Reline::Key.new(c, c, false), Reline::Key.new(escaped_c, escaped_c, false)])
- elsif escaped_c == "\e".ord # escape twice
- block.([Reline::Key.new(c, c, false), Reline::Key.new(c, c, false)])
- else
- block.([Reline::Key.new(escaped_c, escaped_c | 0b10000000, true)])
end
end
@@ -473,7 +412,7 @@ module Reline
end
private def may_req_ambiguous_char_width
- @ambiguous_width = 2 if io_gate == Reline::GeneralIO or !STDOUT.tty?
+ @ambiguous_width = 2 if io_gate.dumb? || !STDIN.tty? || !STDOUT.tty?
return if defined? @ambiguous_width
io_gate.move_cursor_column(0)
begin
@@ -521,8 +460,8 @@ module Reline
def_single_delegator :line_editor, :byte_pointer, :point
def_single_delegator :line_editor, :byte_pointer=, :point=
- def self.insert_text(*args, &block)
- line_editor.insert_text(*args, &block)
+ def self.insert_text(text)
+ line_editor.insert_multiline_text(text)
self
end
@@ -567,37 +506,13 @@ module Reline
def self.line_editor
core.line_editor
end
+end
- def self.update_iogate
- return if core.config.test_mode
- # Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen`
- # Example: rails/spring boot the application in non-tty, then run console in tty.
- if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty?
- require 'reline/ansi'
- remove_const(:IOGate)
- const_set(:IOGate, Reline::ANSI)
- end
- end
-end
+Reline::IOGate = Reline::IO.decide_io_gate
-require 'reline/general_io'
-io = Reline::GeneralIO
-unless ENV['TERM'] == 'dumb'
- case RbConfig::CONFIG['host_os']
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
- require 'reline/windows'
- tty = (io = Reline::Windows).msys_tty?
- else
- tty = $stdout.tty?
- end
-end
-Reline::IOGate = if tty
- require 'reline/ansi'
- Reline::ANSI
-else
- io
-end
+# Deprecated
+Reline::GeneralIO = Reline::Dumb.new
Reline::Face.load_initial_configs
diff --git a/lib/reline/config.rb b/lib/reline/config.rb
index d44c2675ab..6aa6ba8d94 100644
--- a/lib/reline/config.rb
+++ b/lib/reline/config.rb
@@ -29,18 +29,31 @@ class Reline::Config
attr_accessor :autocompletion
def initialize
- @additional_key_bindings = {} # from inputrc
- @additional_key_bindings[:emacs] = {}
- @additional_key_bindings[:vi_insert] = {}
- @additional_key_bindings[:vi_command] = {}
- @oneshot_key_bindings = {}
+ reset_variables
+ end
+
+ def reset
+ if editing_mode_is?(:vi_command)
+ @editing_mode_label = :vi_insert
+ end
+ @oneshot_key_bindings.clear
+ end
+
+ def reset_variables
+ @additional_key_bindings = { # from inputrc
+ emacs: Reline::KeyActor::Base.new,
+ vi_insert: Reline::KeyActor::Base.new,
+ vi_command: Reline::KeyActor::Base.new
+ }
+ @oneshot_key_bindings = Reline::KeyActor::Base.new
@editing_mode_label = :emacs
@keymap_label = :emacs
@keymap_prefix = []
- @key_actors = {}
- @key_actors[:emacs] = Reline::KeyActor::Emacs.new
- @key_actors[:vi_insert] = Reline::KeyActor::ViInsert.new
- @key_actors[:vi_command] = Reline::KeyActor::ViCommand.new
+ @default_key_bindings = {
+ emacs: Reline::KeyActor::Base.new(Reline::KeyActor::EMACS_MAPPING),
+ vi_insert: Reline::KeyActor::Base.new(Reline::KeyActor::VI_INSERT_MAPPING),
+ vi_command: Reline::KeyActor::Base.new(Reline::KeyActor::VI_COMMAND_MAPPING)
+ }
@vi_cmd_mode_string = '(cmd)'
@vi_ins_mode_string = '(ins)'
@emacs_mode_string = '@'
@@ -49,20 +62,15 @@ class Reline::Config
@keyseq_timeout = 500
@test_mode = false
@autocompletion = false
- @convert_meta = true if seven_bit_encoding?(Reline::IOGate.encoding)
+ @convert_meta = seven_bit_encoding?(Reline::IOGate.encoding)
@loaded = false
@enable_bracketed_paste = true
- end
-
- def reset
- if editing_mode_is?(:vi_command)
- @editing_mode_label = :vi_insert
- end
- @oneshot_key_bindings.clear
+ @show_mode_in_prompt = false
+ @default_inputrc_path = nil
end
def editing_mode
- @key_actors[@editing_mode_label]
+ @default_key_bindings[@editing_mode_label]
end
def editing_mode=(val)
@@ -74,7 +82,7 @@ class Reline::Config
end
def keymap
- @key_actors[@keymap_label]
+ @default_key_bindings[@keymap_label]
end
def loaded?
@@ -133,14 +141,14 @@ class Reline::Config
def key_bindings
# The key bindings for each editing mode will be overwritten by the user-defined ones.
- kb = @key_actors[@editing_mode_label].default_key_bindings.dup
- kb.merge!(@additional_key_bindings[@editing_mode_label])
- kb.merge!(@oneshot_key_bindings)
- kb
+ Reline::KeyActor::Composite.new([@oneshot_key_bindings, @additional_key_bindings[@editing_mode_label], @default_key_bindings[@editing_mode_label]])
end
def add_oneshot_key_binding(keystroke, target)
- @oneshot_key_bindings[keystroke] = target
+ # IRB sets invalid keystroke [Reline::Key]. We should ignore it.
+ return unless keystroke.all? { |c| c.is_a?(Integer) }
+
+ @oneshot_key_bindings.add(keystroke, target)
end
def reset_oneshot_key_bindings
@@ -148,11 +156,11 @@ class Reline::Config
end
def add_default_key_binding_by_keymap(keymap, keystroke, target)
- @key_actors[keymap].default_key_bindings[keystroke] = target
+ @default_key_bindings[keymap].add(keystroke, target)
end
def add_default_key_binding(keystroke, target)
- @key_actors[@keymap_label].default_key_bindings[keystroke] = target
+ add_default_key_binding_by_keymap(@keymap_label, keystroke, target)
end
def read_lines(lines, file = nil)
@@ -182,16 +190,17 @@ class Reline::Config
next if if_stack.any? { |_no, skip| skip }
case line
- when /^set +([^ ]+) +([^ ]+)/i
- var, value = $1.downcase, $2
- bind_variable(var, value)
+ when /^set +([^ ]+) +(.+)/i
+ # value ignores everything after a space, raw_value does not.
+ var, value, raw_value = $1.downcase, $2.partition(' ').first, $2
+ bind_variable(var, value, raw_value)
next
when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o
key, func_name = $1, $2
func_name = func_name.split.first
keystroke, func = bind_key(key, func_name)
next unless keystroke
- @additional_key_bindings[@keymap_label][@keymap_prefix + keystroke] = func
+ @additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func)
end
end
unless if_stack.empty?
@@ -234,7 +243,7 @@ class Reline::Config
end
end
- def bind_variable(name, value)
+ def bind_variable(name, value, raw_value)
case name
when 'history-size'
begin
@@ -242,24 +251,8 @@ class Reline::Config
rescue ArgumentError
@history_size = 500
end
- when 'bell-style'
- @bell_style =
- case value
- when 'none', 'off'
- :none
- when 'audible', 'on'
- :audible
- when 'visible'
- :visible
- else
- :audible
- end
- when 'comment-begin'
- @comment_begin = value.dup
- when 'completion-query-items'
- @completion_query_items = value.to_i
when 'isearch-terminators'
- @isearch_terminators = retrieve_string(value)
+ @isearch_terminators = retrieve_string(raw_value)
when 'editing-mode'
case value
when 'emacs'
@@ -301,11 +294,11 @@ class Reline::Config
@show_mode_in_prompt = false
end
when 'vi-cmd-mode-string'
- @vi_cmd_mode_string = retrieve_string(value)
+ @vi_cmd_mode_string = retrieve_string(raw_value)
when 'vi-ins-mode-string'
- @vi_ins_mode_string = retrieve_string(value)
+ @vi_ins_mode_string = retrieve_string(raw_value)
when 'emacs-mode-string'
- @emacs_mode_string = retrieve_string(value)
+ @emacs_mode_string = retrieve_string(raw_value)
when *VARIABLE_NAMES then
variable_name = :"@#{name.tr(?-, ?_)}"
instance_variable_set(variable_name, value.nil? || value == '1' || value == 'on')
@@ -373,6 +366,11 @@ class Reline::Config
ret
end
+ def reload
+ reset_variables
+ read
+ end
+
private def seven_bit_encoding?(encoding)
encoding == Encoding::US_ASCII
end
diff --git a/lib/reline/face.rb b/lib/reline/face.rb
index d07196e2e7..5b4464a623 100644
--- a/lib/reline/face.rb
+++ b/lib/reline/face.rb
@@ -107,7 +107,7 @@ class Reline::Face
def sgr_rgb_256color(key, value)
# 256 colors are
- # 0..15: standard colors, hight intensity colors
+ # 0..15: standard colors, high intensity colors
# 16..232: 216 colors (R, G, B each 6 steps)
# 233..255: grayscale colors (24 steps)
# This methods converts rgb_expression to 216 colors
diff --git a/lib/reline/general_io.rb b/lib/reline/general_io.rb
deleted file mode 100644
index d52151ad3c..0000000000
--- a/lib/reline/general_io.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'io/wait'
-
-class Reline::GeneralIO
- RESET_COLOR = '' # Do not send color reset sequence
-
- def self.reset(encoding: nil)
- @@pasting = false
- if encoding
- @@encoding = encoding
- elsif defined?(@@encoding)
- remove_class_variable(:@@encoding)
- end
- end
-
- def self.encoding
- if defined?(@@encoding)
- @@encoding
- elsif RUBY_PLATFORM =~ /mswin|mingw/
- Encoding::UTF_8
- else
- Encoding::default_external
- end
- end
-
- def self.win?
- false
- end
-
- def self.set_default_key_bindings(_)
- end
-
- @@buf = []
- @@input = STDIN
-
- def self.input=(val)
- @@input = val
- end
-
- def self.with_raw_input
- yield
- end
-
- def self.getc(_timeout_second)
- unless @@buf.empty?
- return @@buf.shift
- end
- c = nil
- loop do
- Reline.core.line_editor.handle_signal
- result = @@input.wait_readable(0.1)
- next if result.nil?
- c = @@input.read(1)
- break
- end
- c&.ord
- end
-
- def self.ungetc(c)
- @@buf.unshift(c)
- end
-
- def self.get_screen_size
- [24, 80]
- end
-
- def self.cursor_pos
- Reline::CursorPos.new(1, 1)
- end
-
- def self.hide_cursor
- end
-
- def self.show_cursor
- end
-
- def self.move_cursor_column(val)
- end
-
- def self.move_cursor_up(val)
- end
-
- def self.move_cursor_down(val)
- end
-
- def self.erase_after_cursor
- end
-
- def self.scroll_down(val)
- end
-
- def self.clear_screen
- end
-
- def self.set_screen_size(rows, columns)
- end
-
- def self.set_winch_handler(&handler)
- end
-
- @@pasting = false
-
- def self.in_pasting?
- @@pasting
- end
-
- def self.prep
- end
-
- def self.deprep(otio)
- end
-end
diff --git a/lib/reline/io.rb b/lib/reline/io.rb
new file mode 100644
index 0000000000..c1dd1a56c8
--- /dev/null
+++ b/lib/reline/io.rb
@@ -0,0 +1,41 @@
+
+module Reline
+ class IO
+ RESET_COLOR = "\e[0m"
+
+ def self.decide_io_gate
+ if ENV['TERM'] == 'dumb'
+ Reline::Dumb.new
+ else
+ require 'reline/io/ansi'
+
+ case RbConfig::CONFIG['host_os']
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
+ require 'reline/io/windows'
+ io = Reline::Windows.new
+ if io.msys_tty?
+ Reline::ANSI.new
+ else
+ io
+ end
+ else
+ Reline::ANSI.new
+ end
+ end
+ end
+
+ def dumb?
+ false
+ end
+
+ def win?
+ false
+ end
+
+ def reset_color_sequence
+ self.class::RESET_COLOR
+ end
+ end
+end
+
+require 'reline/io/dumb'
diff --git a/lib/reline/ansi.rb b/lib/reline/io/ansi.rb
index 45a475a787..a730a953f7 100644
--- a/lib/reline/ansi.rb
+++ b/lib/reline/io/ansi.rb
@@ -1,10 +1,7 @@
require 'io/console'
require 'io/wait'
-require_relative 'terminfo'
-
-class Reline::ANSI
- RESET_COLOR = "\e[0m"
+class Reline::ANSI < Reline::IO
CAPNAME_KEY_BINDINGS = {
'khome' => :ed_move_to_beg,
'kend' => :ed_move_to_end,
@@ -36,15 +33,18 @@ class Reline::ANSI
Reline::Terminfo.setupterm(0, 2)
end
- def self.encoding
- Encoding.default_external
+ def initialize
+ @input = STDIN
+ @output = STDOUT
+ @buf = []
+ @old_winch_handler = nil
end
- def self.win?
- false
+ def encoding
+ Encoding.default_external
end
- def self.set_default_key_bindings(config, allow_terminfo: true)
+ def set_default_key_bindings(config, allow_terminfo: true)
set_bracketed_paste_key_bindings(config)
set_default_key_bindings_ansi_cursor(config)
if allow_terminfo && Reline::Terminfo.enabled?
@@ -67,13 +67,13 @@ class Reline::ANSI
end
end
- def self.set_bracketed_paste_key_bindings(config)
+ def set_bracketed_paste_key_bindings(config)
[:emacs, :vi_insert, :vi_command].each do |keymap|
config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
end
end
- def self.set_default_key_bindings_ansi_cursor(config)
+ def set_default_key_bindings_ansi_cursor(config)
ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
bindings = [["\e[#{char}", default_func]] # CSI + char
if modifiers[:ctrl]
@@ -95,7 +95,7 @@ class Reline::ANSI
end
end
- def self.set_default_key_bindings_terminfo(config)
+ def set_default_key_bindings_terminfo(config)
key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding|
begin
key_code = Reline::Terminfo.tigetstr(capname)
@@ -112,12 +112,16 @@ class Reline::ANSI
end
end
- def self.set_default_key_bindings_comprehensive_list(config)
+ def set_default_key_bindings_comprehensive_list(config)
{
+ # xterm
+ [27, 91, 51, 126] => :key_delete, # kdch1
+ [27, 91, 53, 126] => :ed_search_prev_history, # kpp
+ [27, 91, 54, 126] => :ed_search_next_history, # knp
+
# Console (80x25)
[27, 91, 49, 126] => :ed_move_to_beg, # Home
[27, 91, 52, 126] => :ed_move_to_end, # End
- [27, 91, 51, 126] => :key_delete, # Del
# KDE
# Del is 0x08
@@ -147,47 +151,42 @@ class Reline::ANSI
end
end
- @@input = STDIN
- def self.input=(val)
- @@input = val
+ def input=(val)
+ @input = val
end
- @@output = STDOUT
- def self.output=(val)
- @@output = val
+ def output=(val)
+ @output = val
end
- def self.with_raw_input
- if @@input.tty?
- @@input.raw(intr: true) { yield }
+ def with_raw_input
+ if @input.tty?
+ @input.raw(intr: true) { yield }
else
yield
end
end
- @@buf = []
- def self.inner_getc(timeout_second)
- unless @@buf.empty?
- return @@buf.shift
+ def inner_getc(timeout_second)
+ unless @buf.empty?
+ return @buf.shift
end
- until @@input.wait_readable(0.01)
+ until @input.wait_readable(0.01)
timeout_second -= 0.01
return nil if timeout_second <= 0
Reline.core.line_editor.handle_signal
end
- c = @@input.getbyte
- (c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c
+ c = @input.getbyte
+ (c == 0x16 && @input.tty? && @input.raw(min: 0, time: 0, &:getbyte)) || c
rescue Errno::EIO
# Maybe the I/O has been closed.
nil
- rescue Errno::ENOTTY
- nil
end
START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
- def self.read_bracketed_paste
+ def read_bracketed_paste
buffer = String.new(encoding: Encoding::ASCII_8BIT)
until buffer.end_with?(END_BRACKETED_PASTE)
c = inner_getc(Float::INFINITY)
@@ -199,60 +198,60 @@ class Reline::ANSI
end
# if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
- def self.getc(timeout_second)
+ def getc(timeout_second)
inner_getc(timeout_second)
end
- def self.in_pasting?
+ def in_pasting?
not empty_buffer?
end
- def self.empty_buffer?
- unless @@buf.empty?
+ def empty_buffer?
+ unless @buf.empty?
return false
end
- !@@input.wait_readable(0)
+ !@input.wait_readable(0)
end
- def self.ungetc(c)
- @@buf.unshift(c)
+ def ungetc(c)
+ @buf.unshift(c)
end
- def self.retrieve_keybuffer
+ def retrieve_keybuffer
begin
- return unless @@input.wait_readable(0.001)
- str = @@input.read_nonblock(1024)
+ return unless @input.wait_readable(0.001)
+ str = @input.read_nonblock(1024)
str.bytes.each do |c|
- @@buf.push(c)
+ @buf.push(c)
end
rescue EOFError
end
end
- def self.get_screen_size
- s = @@input.winsize
+ def get_screen_size
+ s = @input.winsize
return s if s[0] > 0 && s[1] > 0
s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
return s if s[0] > 0 && s[1] > 0
[24, 80]
- rescue Errno::ENOTTY
+ rescue Errno::ENOTTY, Errno::ENODEV
[24, 80]
end
- def self.set_screen_size(rows, columns)
- @@input.winsize = [rows, columns]
+ def set_screen_size(rows, columns)
+ @input.winsize = [rows, columns]
self
- rescue Errno::ENOTTY
+ rescue Errno::ENOTTY, Errno::ENODEV
self
end
- def self.cursor_pos
- begin
+ def cursor_pos
+ if both_tty?
res = +''
m = nil
- @@input.raw do |stdin|
- @@output << "\e[6n"
- @@output.flush
+ @input.raw do |stdin|
+ @output << "\e[6n"
+ @output.flush
loop do
c = stdin.getc
next if c.nil?
@@ -266,9 +265,9 @@ class Reline::ANSI
end
column = m[:column].to_i - 1
row = m[:row].to_i - 1
- rescue Errno::ENOTTY
+ else
begin
- buf = @@output.pread(@@output.pos, 0)
+ buf = @output.pread(@output.pos, 0)
row = buf.count("\n")
column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
rescue Errno::ESPIPE, IOError
@@ -281,82 +280,93 @@ class Reline::ANSI
Reline::CursorPos.new(column, row)
end
- def self.move_cursor_column(x)
- @@output.write "\e[#{x + 1}G"
+ def both_tty?
+ @input.tty? && @output.tty?
+ end
+
+ def move_cursor_column(x)
+ @output.write "\e[#{x + 1}G"
end
- def self.move_cursor_up(x)
+ def move_cursor_up(x)
if x > 0
- @@output.write "\e[#{x}A"
+ @output.write "\e[#{x}A"
elsif x < 0
move_cursor_down(-x)
end
end
- def self.move_cursor_down(x)
+ def move_cursor_down(x)
if x > 0
- @@output.write "\e[#{x}B"
+ @output.write "\e[#{x}B"
elsif x < 0
move_cursor_up(-x)
end
end
- def self.hide_cursor
+ def hide_cursor
+ seq = "\e[?25l"
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin
- @@output.write Reline::Terminfo.tigetstr('civis')
+ seq = Reline::Terminfo.tigetstr('civis')
rescue Reline::Terminfo::TerminfoError
# civis is undefined
end
- else
- # ignored
end
+ @output.write seq
end
- def self.show_cursor
+ def show_cursor
+ seq = "\e[?25h"
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin
- @@output.write Reline::Terminfo.tigetstr('cnorm')
+ seq = Reline::Terminfo.tigetstr('cnorm')
rescue Reline::Terminfo::TerminfoError
# cnorm is undefined
end
- else
- # ignored
end
+ @output.write seq
end
- def self.erase_after_cursor
- @@output.write "\e[K"
+ def erase_after_cursor
+ @output.write "\e[K"
end
# This only works when the cursor is at the bottom of the scroll range
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
- def self.scroll_down(x)
+ def scroll_down(x)
return if x.zero?
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
- @@output.write "\n" * x
+ @output.write "\n" * x
end
- def self.clear_screen
- @@output.write "\e[2J"
- @@output.write "\e[1;1H"
+ def clear_screen
+ @output.write "\e[2J"
+ @output.write "\e[1;1H"
end
- @@old_winch_handler = nil
- def self.set_winch_handler(&handler)
- @@old_winch_handler = Signal.trap('WINCH', &handler)
+ def set_winch_handler(&handler)
+ @old_winch_handler = Signal.trap('WINCH', &handler)
+ @old_cont_handler = Signal.trap('CONT') do
+ @input.raw!(intr: true) if @input.tty?
+ # Rerender the screen. Note that screen size might be changed while suspended.
+ handler.call
+ end
+ rescue ArgumentError
+ # Signal.trap may raise an ArgumentError if the platform doesn't support the signal.
end
- def self.prep
+ def prep
# Enable bracketed paste
- @@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste
+ @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty?
retrieve_keybuffer
nil
end
- def self.deprep(otio)
+ def deprep(otio)
# Disable bracketed paste
- @@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
- Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler
+ @output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty?
+ Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
+ Signal.trap('CONT', @old_cont_handler) if @old_cont_handler
end
end
diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb
new file mode 100644
index 0000000000..6ed69ffdfa
--- /dev/null
+++ b/lib/reline/io/dumb.rb
@@ -0,0 +1,106 @@
+require 'io/wait'
+
+class Reline::Dumb < Reline::IO
+ RESET_COLOR = '' # Do not send color reset sequence
+
+ def initialize(encoding: nil)
+ @input = STDIN
+ @buf = []
+ @pasting = false
+ @encoding = encoding
+ @screen_size = [24, 80]
+ end
+
+ def dumb?
+ true
+ end
+
+ def encoding
+ if @encoding
+ @encoding
+ elsif RUBY_PLATFORM =~ /mswin|mingw/
+ Encoding::UTF_8
+ else
+ Encoding::default_external
+ end
+ end
+
+ def set_default_key_bindings(_)
+ end
+
+ def input=(val)
+ @input = val
+ end
+
+ def with_raw_input
+ yield
+ end
+
+ def getc(_timeout_second)
+ unless @buf.empty?
+ return @buf.shift
+ end
+ c = nil
+ loop do
+ Reline.core.line_editor.handle_signal
+ result = @input.wait_readable(0.1)
+ next if result.nil?
+ c = @input.read(1)
+ break
+ end
+ c&.ord
+ end
+
+ def ungetc(c)
+ @buf.unshift(c)
+ end
+
+ def get_screen_size
+ @screen_size
+ end
+
+ def cursor_pos
+ Reline::CursorPos.new(1, 1)
+ end
+
+ def hide_cursor
+ end
+
+ def show_cursor
+ end
+
+ def move_cursor_column(val)
+ end
+
+ def move_cursor_up(val)
+ end
+
+ def move_cursor_down(val)
+ end
+
+ def erase_after_cursor
+ end
+
+ def scroll_down(val)
+ end
+
+ def clear_screen
+ end
+
+ def set_screen_size(rows, columns)
+ @screen_size = [rows, columns]
+ end
+
+ def set_winch_handler(&handler)
+ end
+
+ def in_pasting?
+ @pasting
+ end
+
+ def prep
+ end
+
+ def deprep(otio)
+ end
+end
diff --git a/lib/reline/windows.rb b/lib/reline/io/windows.rb
index ee3f73e383..40025db504 100644
--- a/lib/reline/windows.rb
+++ b/lib/reline/io/windows.rb
@@ -1,21 +1,49 @@
require 'fiddle/import'
-class Reline::Windows
- RESET_COLOR = "\e[0m"
+class Reline::Windows < Reline::IO
+ def initialize
+ @input_buf = []
+ @output_buf = []
+
+ @output = STDOUT
+ @hsg = nil
+ @getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
+ @kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
+ @GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
+ @GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
+ @SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
+ @GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
+ @FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
+ @ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
+ @hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE)
+ @hConsoleInputHandle = @GetStdHandle.call(STD_INPUT_HANDLE)
+ @GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
+ @ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
+ @GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
+ @GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
+ @FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
+ @SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
+
+ @GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
+ @SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
+ @WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
+
+ @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
+ end
- def self.encoding
+ def encoding
Encoding::UTF_8
end
- def self.win?
+ def win?
true
end
- def self.win_legacy_console?
- @@legacy_console
+ def win_legacy_console?
+ @legacy_console
end
- def self.set_default_key_bindings(config)
+ def set_default_key_bindings(config)
{
[224, 72] => :ed_prev_history, # ↑
[224, 80] => :ed_next_history, # ↓
@@ -129,58 +157,42 @@ class Reline::Windows
STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003
FILE_NAME_INFO = 2
- @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
- @@kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
- @@GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
- @@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
- @@SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
- @@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
- @@FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
- @@ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
- @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
- @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
- @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
- @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
- @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
- @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
- @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
- @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
-
- @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
- @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
- @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
- private_class_method def self.getconsolemode
+ # Calling Win32API with console handle is reported to fail after executing some external command.
+ # We need to refresh console handle and retry the call again.
+ private def call_with_console_handle(win32func, *args)
+ val = win32func.call(@hConsoleHandle, *args)
+ return val if val != 0
+
+ @hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE)
+ win32func.call(@hConsoleHandle, *args)
+ end
+
+ private def getconsolemode
mode = "\000\000\000\000"
- @@GetConsoleMode.call(@@hConsoleHandle, mode)
+ call_with_console_handle(@GetConsoleMode, mode)
mode.unpack1('L')
end
- private_class_method def self.setconsolemode(mode)
- @@SetConsoleMode.call(@@hConsoleHandle, mode)
+ private def setconsolemode(mode)
+ call_with_console_handle(@SetConsoleMode, mode)
end
- @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
- #if @@legacy_console
+ #if @legacy_console
# setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
- # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
+ # @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
#end
- @@input_buf = []
- @@output_buf = []
-
- @@output = STDOUT
-
- def self.msys_tty?(io = @@hConsoleInputHandle)
+ def msys_tty?(io = @hConsoleInputHandle)
# check if fd is a pipe
- if @@GetFileType.call(io) != FILE_TYPE_PIPE
+ if @GetFileType.call(io) != FILE_TYPE_PIPE
return false
end
bufsize = 1024
p_buffer = "\0" * bufsize
- res = @@GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2)
+ res = @GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2)
return false if res == 0
# get pipe name: p_buffer layout is:
@@ -217,65 +229,63 @@ class Reline::Windows
[ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ],
]
- @@hsg = nil
-
- def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
+ def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
# high-surrogate
if 0xD800 <= char_code and char_code <= 0xDBFF
- @@hsg = char_code
+ @hsg = char_code
return
end
# low-surrogate
if 0xDC00 <= char_code and char_code <= 0xDFFF
- if @@hsg
- char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00
- @@hsg = nil
+ if @hsg
+ char_code = 0x10000 + (@hsg - 0xD800) * 0x400 + char_code - 0xDC00
+ @hsg = nil
else
# no high-surrogate. ignored.
return
end
else
# ignore high-surrogate without low-surrogate if there
- @@hsg = nil
+ @hsg = nil
end
key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
match = KEY_MAP.find { |args,| key.matches?(**args) }
unless match.nil?
- @@output_buf.concat(match.last)
+ @output_buf.concat(match.last)
return
end
# no char, only control keys
return if key.char_code == 0 and key.control_keys.any?
- @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL)
+ @output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL)
- @@output_buf.concat(key.char.bytes)
+ @output_buf.concat(key.char.bytes)
end
- def self.check_input_event
+ def check_input_event
num_of_events = 0.chr * 8
- while @@output_buf.empty?
+ while @output_buf.empty?
Reline.core.line_editor.handle_signal
- if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec
+ if @WaitForSingleObject.(@hConsoleInputHandle, 100) != 0 # max 0.1 sec
# prevent for background consolemode change
- @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
+ @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
next
end
- next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
+ next if @GetNumberOfConsoleInputEvents.(@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
input_records = 0.chr * 20 * 80
read_event = 0.chr * 4
- if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_records, 80, read_event) != 0
+ if @ReadConsoleInputW.(@hConsoleInputHandle, input_records, 80, read_event) != 0
read_events = read_event.unpack1('L')
0.upto(read_events) do |idx|
input_record = input_records[idx * 20, 20]
event = input_record[0, 2].unpack1('s*')
case event
when WINDOW_BUFFER_SIZE_EVENT
- @@winch_handler.()
+ @winch_handler.()
when KEY_EVENT
key_down = input_record[4, 4].unpack1('l*')
repeat_count = input_record[8, 2].unpack1('s*')
@@ -293,34 +303,34 @@ class Reline::Windows
end
end
- def self.with_raw_input
+ def with_raw_input
yield
end
- def self.getc(_timeout_second)
+ def getc(_timeout_second)
check_input_event
- @@output_buf.shift
+ @output_buf.shift
end
- def self.ungetc(c)
- @@output_buf.unshift(c)
+ def ungetc(c)
+ @output_buf.unshift(c)
end
- def self.in_pasting?
- not self.empty_buffer?
+ def in_pasting?
+ not empty_buffer?
end
- def self.empty_buffer?
- if not @@output_buf.empty?
+ def empty_buffer?
+ if not @output_buf.empty?
false
- elsif @@kbhit.call == 0
+ elsif @kbhit.call == 0
true
else
false
end
end
- def self.get_console_screen_buffer_info
+ def get_console_screen_buffer_info
# CONSOLE_SCREEN_BUFFER_INFO
# [ 0,2] dwSize.X
# [ 2,2] dwSize.Y
@@ -334,18 +344,18 @@ class Reline::Windows
# [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22
- return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
+ return if call_with_console_handle(@GetConsoleScreenBufferInfo, csbi) == 0
csbi
end
- def self.get_screen_size
+ def get_screen_size
unless csbi = get_console_screen_buffer_info
return [1, 1]
end
csbi[0, 4].unpack('SS').reverse
end
- def self.cursor_pos
+ def cursor_pos
unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0)
end
@@ -354,49 +364,49 @@ class Reline::Windows
Reline::CursorPos.new(x, y)
end
- def self.move_cursor_column(val)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val)
+ def move_cursor_column(val)
+ call_with_console_handle(@SetConsoleCursorPosition, cursor_pos.y * 65536 + val)
end
- def self.move_cursor_up(val)
+ def move_cursor_up(val)
if val > 0
y = cursor_pos.y - val
y = 0 if y < 0
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x)
+ call_with_console_handle(@SetConsoleCursorPosition, y * 65536 + cursor_pos.x)
elsif val < 0
move_cursor_down(-val)
end
end
- def self.move_cursor_down(val)
+ def move_cursor_down(val)
if val > 0
return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first
y = cursor_pos.y + val
y = screen_height - 1 if y > (screen_height - 1)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x)
+ call_with_console_handle(@SetConsoleCursorPosition, (cursor_pos.y + val) * 65536 + cursor_pos.x)
elsif val < 0
move_cursor_up(-val)
end
end
- def self.erase_after_cursor
+ def erase_after_cursor
return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L')
written = 0.chr * 4
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
+ call_with_console_handle(@FillConsoleOutputCharacter, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
+ call_with_console_handle(@FillConsoleOutputAttribute, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
end
- def self.scroll_down(val)
+ def scroll_down(val)
return if val < 0
return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height
- if @@legacy_console || window_left != 0
+ if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console
# and puts "\n" causes horizontal scroll. its glitch.
@@ -404,11 +414,11 @@ class Reline::Windows
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS')
- @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
+ call_with_console_handle(@ScrollConsoleScreenBuffer, scroll_rectangle, nil, destination_origin, fill)
else
origin_x = x + 1
origin_y = y - window_top + 1
- @@output.write [
+ @output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
@@ -416,49 +426,49 @@ class Reline::Windows
end
end
- def self.clear_screen
- if @@legacy_console
+ def clear_screen
+ if @legacy_console
return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536
written = 0.chr * 4
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft)
+ call_with_console_handle(@FillConsoleOutputCharacter, 0x20, fill_length, screen_topleft, written)
+ call_with_console_handle(@FillConsoleOutputAttribute, attributes, fill_length, screen_topleft, written)
+ call_with_console_handle(@SetConsoleCursorPosition, screen_topleft)
else
- @@output.write "\e[2J" "\e[H"
+ @output.write "\e[2J" "\e[H"
end
end
- def self.set_screen_size(rows, columns)
+ def set_screen_size(rows, columns)
raise NotImplementedError
end
- def self.hide_cursor
+ def hide_cursor
size = 100
visible = 0 # 0 means false
cursor_info = [size, visible].pack('Li')
- @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
+ call_with_console_handle(@SetConsoleCursorInfo, cursor_info)
end
- def self.show_cursor
+ def show_cursor
size = 100
visible = 1 # 1 means true
cursor_info = [size, visible].pack('Li')
- @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
+ call_with_console_handle(@SetConsoleCursorInfo, cursor_info)
end
- def self.set_winch_handler(&handler)
- @@winch_handler = handler
+ def set_winch_handler(&handler)
+ @winch_handler = handler
end
- def self.prep
+ def prep
# do nothing
nil
end
- def self.deprep(otio)
+ def deprep(otio)
# do nothing
end
diff --git a/lib/reline/key_actor.rb b/lib/reline/key_actor.rb
index ebe09d2009..0ac7604556 100644
--- a/lib/reline/key_actor.rb
+++ b/lib/reline/key_actor.rb
@@ -2,6 +2,7 @@ module Reline::KeyActor
end
require 'reline/key_actor/base'
+require 'reline/key_actor/composite'
require 'reline/key_actor/emacs'
require 'reline/key_actor/vi_command'
require 'reline/key_actor/vi_insert'
diff --git a/lib/reline/key_actor/base.rb b/lib/reline/key_actor/base.rb
index 194e98938c..ee28c7681e 100644
--- a/lib/reline/key_actor/base.rb
+++ b/lib/reline/key_actor/base.rb
@@ -1,15 +1,31 @@
class Reline::KeyActor::Base
- MAPPING = Array.new(256)
+ def initialize(mapping = [])
+ @mapping = mapping
+ @matching_bytes = {}
+ @key_bindings = {}
+ end
def get_method(key)
- self.class::MAPPING[key]
+ @mapping[key]
+ end
+
+ def add(key, func)
+ (1...key.size).each do |size|
+ @matching_bytes[key.take(size)] = true
+ end
+ @key_bindings[key] = func
+ end
+
+ def matching?(key)
+ @matching_bytes[key]
end
- def initialize
- @default_key_bindings = {}
+ def get(key)
+ @key_bindings[key]
end
- def default_key_bindings
- @default_key_bindings
+ def clear
+ @matching_bytes.clear
+ @key_bindings.clear
end
end
diff --git a/lib/reline/key_actor/composite.rb b/lib/reline/key_actor/composite.rb
new file mode 100644
index 0000000000..37e94ce6cf
--- /dev/null
+++ b/lib/reline/key_actor/composite.rb
@@ -0,0 +1,17 @@
+class Reline::KeyActor::Composite
+ def initialize(key_actors)
+ @key_actors = key_actors
+ end
+
+ def matching?(key)
+ @key_actors.any? { |key_actor| key_actor.matching?(key) }
+ end
+
+ def get(key)
+ @key_actors.each do |key_actor|
+ func = key_actor.get(key)
+ return func if func
+ end
+ nil
+ end
+end
diff --git a/lib/reline/key_actor/emacs.rb b/lib/reline/key_actor/emacs.rb
index 9c797ba43e..ad84ee1d99 100644
--- a/lib/reline/key_actor/emacs.rb
+++ b/lib/reline/key_actor/emacs.rb
@@ -1,5 +1,5 @@
-class Reline::KeyActor::Emacs < Reline::KeyActor::Base
- MAPPING = [
+module Reline::KeyActor
+ EMACS_MAPPING = [
# 0 ^@
:em_set_mark,
# 1 ^A
@@ -63,7 +63,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 30 ^^
:ed_unassigned,
# 31 ^_
- :ed_unassigned,
+ :undo,
# 32 SPACE
:ed_insert,
# 33 !
@@ -319,7 +319,7 @@ class Reline::KeyActor::Emacs < Reline::KeyActor::Base
# 158 M-^^
:ed_unassigned,
# 159 M-^_
- :ed_unassigned,
+ :redo,
# 160 M-SPACE
:em_set_mark,
# 161 M-!
diff --git a/lib/reline/key_actor/vi_command.rb b/lib/reline/key_actor/vi_command.rb
index 06bb0ba8e4..d972c5e67f 100644
--- a/lib/reline/key_actor/vi_command.rb
+++ b/lib/reline/key_actor/vi_command.rb
@@ -1,5 +1,5 @@
-class Reline::KeyActor::ViCommand < Reline::KeyActor::Base
- MAPPING = [
+module Reline::KeyActor
+ VI_COMMAND_MAPPING = [
# 0 ^@
:ed_unassigned,
# 1 ^A
diff --git a/lib/reline/key_actor/vi_insert.rb b/lib/reline/key_actor/vi_insert.rb
index f8ccf468c6..312df1646b 100644
--- a/lib/reline/key_actor/vi_insert.rb
+++ b/lib/reline/key_actor/vi_insert.rb
@@ -1,5 +1,5 @@
-class Reline::KeyActor::ViInsert < Reline::KeyActor::Base
- MAPPING = [
+module Reline::KeyActor
+ VI_INSERT_MAPPING = [
# 0 ^@
:ed_unassigned,
# 1 ^A
diff --git a/lib/reline/key_stroke.rb b/lib/reline/key_stroke.rb
index bceffbb53f..ba40899685 100644
--- a/lib/reline/key_stroke.rb
+++ b/lib/reline/key_stroke.rb
@@ -7,138 +7,99 @@ class Reline::KeyStroke
@config = config
end
- def compress_meta_key(ary)
- return ary unless @config.convert_meta
- ary.inject([]) { |result, key|
- if result.size > 0 and result.last == "\e".ord
- result[result.size - 1] = Reline::Key.new(key, key | 0b10000000, true)
- else
- result << key
- end
- result
- }
- end
+ # Input exactly matches to a key sequence
+ MATCHING = :matching
+ # Input partially matches to a key sequence
+ MATCHED = :matched
+ # Input matches to a key sequence and the key sequence is a prefix of another key sequence
+ MATCHING_MATCHED = :matching_matched
+ # Input does not match to any key sequence
+ UNMATCHED = :unmatched
- def start_with?(me, other)
- compressed_me = compress_meta_key(me)
- compressed_other = compress_meta_key(other)
- i = 0
- loop do
- my_c = compressed_me[i]
- other_c = compressed_other[i]
- other_is_last = (i + 1) == compressed_other.size
- me_is_last = (i + 1) == compressed_me.size
- if my_c != other_c
- if other_c == "\e".ord and other_is_last and my_c.is_a?(Reline::Key) and my_c.with_meta
- return true
- else
- return false
- end
- elsif other_is_last
- return true
- elsif me_is_last
- return false
- end
- i += 1
- end
- end
+ def match_status(input)
+ matching = key_mapping.matching?(input)
+ matched = key_mapping.get(input)
- def equal?(me, other)
- case me
- when Array
- compressed_me = compress_meta_key(me)
- compressed_other = compress_meta_key(other)
- compressed_me.size == compressed_other.size and [compressed_me, compressed_other].transpose.all?{ |i| equal?(i[0], i[1]) }
- when Integer
- if other.is_a?(Reline::Key)
- if other.combined_char == "\e".ord
- false
- else
- other.combined_char == me
- end
- else
- me == other
- end
- when Reline::Key
- if other.is_a?(Integer)
- me.combined_char == other
- else
- me == other
- end
- end
- end
+ # FIXME: Workaround for single byte. remove this after MAPPING is merged into KeyActor.
+ matched ||= input.size == 1
+ matching ||= input == [ESC_BYTE]
- def match_status(input)
- key_mapping.keys.select { |lhs|
- start_with?(lhs, input)
- }.tap { |it|
- return :matched if it.size == 1 && equal?(it[0], input)
- return :matching if it.size == 1 && !equal?(it[0], input)
- return :matched if it.max_by(&:size)&.size&.< input.size
- return :matching if it.size > 1
- }
- if key_mapping.keys.any? { |lhs| start_with?(input, lhs) }
- :matched
+ if matching && matched
+ MATCHING_MATCHED
+ elsif matching
+ MATCHING
+ elsif matched
+ MATCHED
+ elsif input[0] == ESC_BYTE
+ match_unknown_escape_sequence(input, vi_mode: @config.editing_mode_is?(:vi_insert, :vi_command))
+ elsif input.size == 1
+ MATCHED
else
- match_unknown_escape_sequence(input).first
+ UNMATCHED
end
end
def expand(input)
- lhs = key_mapping.keys.select { |item| start_with?(input, item) }.sort_by(&:size).last
- unless lhs
- status, size = match_unknown_escape_sequence(input)
- case status
- when :matched
- return [:ed_unassigned] + expand(input.drop(size))
- when :matching
- return [:ed_unassigned]
- else
- return input
- end
+ matched_bytes = nil
+ (1..input.size).each do |i|
+ bytes = input.take(i)
+ status = match_status(bytes)
+ matched_bytes = bytes if status == MATCHED || status == MATCHING_MATCHED
end
- rhs = key_mapping[lhs]
+ return [[], []] unless matched_bytes
- case rhs
- when String
- rhs_bytes = rhs.bytes
- expand(expand(rhs_bytes) + expand(input.drop(lhs.size)))
- when Symbol
- [rhs] + expand(input.drop(lhs.size))
- when Array
- rhs
+ func = key_mapping.get(matched_bytes)
+ if func.is_a?(Array)
+ keys = func.map { |c| Reline::Key.new(c, c, false) }
+ elsif func
+ keys = [Reline::Key.new(func, func, false)]
+ elsif matched_bytes.size == 1
+ keys = [Reline::Key.new(matched_bytes.first, matched_bytes.first, false)]
+ elsif matched_bytes.size == 2 && matched_bytes[0] == ESC_BYTE
+ keys = [Reline::Key.new(matched_bytes[1], matched_bytes[1] | 0b10000000, true)]
+ else
+ keys = []
end
+
+ [keys, input.drop(matched_bytes.size)]
end
private
# returns match status of CSI/SS3 sequence and matched length
- def match_unknown_escape_sequence(input)
+ def match_unknown_escape_sequence(input, vi_mode: false)
idx = 0
- return [:unmatched, nil] unless input[idx] == ESC_BYTE
+ return UNMATCHED unless input[idx] == ESC_BYTE
idx += 1
idx += 1 if input[idx] == ESC_BYTE
case input[idx]
when nil
- return [:matching, nil]
+ if idx == 1 # `ESC`
+ return MATCHING_MATCHED
+ else # `ESC ESC`
+ return MATCHING
+ end
when 91 # == '['.ord
- # CSI sequence
+ # CSI sequence `ESC [ ... char`
idx += 1
idx += 1 while idx < input.size && CSI_PARAMETER_BYTES_RANGE.cover?(input[idx])
idx += 1 while idx < input.size && CSI_INTERMEDIATE_BYTES_RANGE.cover?(input[idx])
- input[idx] ? [:matched, idx + 1] : [:matching, nil]
when 79 # == 'O'.ord
- # SS3 sequence
- input[idx + 1] ? [:matched, idx + 2] : [:matching, nil]
+ # SS3 sequence `ESC O char`
+ idx += 1
else
- if idx == 1
- # `ESC char`, make it :unmatched so that it will be handled correctly in `read_2nd_character_of_key_sequence`
- [:unmatched, nil]
- else
- # `ESC ESC char`
- [:matched, idx + 1]
- end
+ # `ESC char` or `ESC ESC char`
+ return UNMATCHED if vi_mode
+ end
+
+ case input.size
+ when idx
+ MATCHING
+ when idx + 1
+ MATCHED
+ else
+ UNMATCHED
end
end
diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb
index 4c76932c10..c71a5f79ee 100644
--- a/lib/reline/line_editor.rb
+++ b/lib/reline/line_editor.rb
@@ -4,7 +4,6 @@ require 'reline/unicode'
require 'tempfile'
class Reline::LineEditor
- # TODO: undo
# TODO: Use "private alias_method" idiom after drop Ruby 2.5.
attr_reader :byte_pointer
attr_accessor :confirm_multiline_termination_proc
@@ -46,6 +45,7 @@ class Reline::LineEditor
RenderedScreen = Struct.new(:base_y, :lines, :cursor_y, keyword_init: true)
CompletionJourneyState = Struct.new(:line_index, :pre, :target, :post, :list, :pointer)
+ NullActionState = [nil, nil].freeze
class MenuInfo
attr_reader :list
@@ -176,9 +176,8 @@ class Reline::LineEditor
scroll_into_view
Reline::IOGate.move_cursor_up @rendered_screen.cursor_y
@rendered_screen.base_y = Reline::IOGate.cursor_pos.y
- @rendered_screen.lines = []
- @rendered_screen.cursor_y = 0
- render_differential
+ clear_rendered_screen_cache
+ render
end
private def handle_interrupted
@@ -186,11 +185,11 @@ class Reline::LineEditor
@interrupted = false
clear_dialogs
- scrolldown = render_differential
- Reline::IOGate.scroll_down scrolldown
+ render
+ cursor_to_bottom_offset = @rendered_screen.lines.size - @rendered_screen.cursor_y
+ Reline::IOGate.scroll_down cursor_to_bottom_offset
Reline::IOGate.move_cursor_column 0
- @rendered_screen.lines = []
- @rendered_screen.cursor_y = 0
+ clear_rendered_screen_cache
case @old_trap
when 'DEFAULT', 'SYSTEM_DEFAULT'
raise Interrupt
@@ -238,7 +237,6 @@ class Reline::LineEditor
@perfect_matched = nil
@menu_info = nil
@searching_prompt = nil
- @first_char = true
@just_cursor_moving = false
@eof = false
@continuous_insertion_buffer = String.new(encoding: @encoding)
@@ -251,6 +249,11 @@ class Reline::LineEditor
@resized = false
@cache = {}
@rendered_screen = RenderedScreen.new(base_y: 0, lines: [], cursor_y: 0)
+ @input_lines = [[[""], 0, 0]]
+ @input_lines_position = 0
+ @undoing = false
+ @prev_action_state = NullActionState
+ @next_action_state = NullActionState
reset_line
end
@@ -410,7 +413,7 @@ class Reline::LineEditor
# do nothing
elsif level == :blank
Reline::IOGate.move_cursor_column base_x
- @output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}"
+ @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
else
x, w, content = new_items[level]
cover_begin = base_x != 0 && new_levels[base_x - 1] == level
@@ -420,7 +423,7 @@ class Reline::LineEditor
content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
end
Reline::IOGate.move_cursor_column x + pos
- @output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}"
+ @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
end
base_x += width
end
@@ -456,28 +459,7 @@ class Reline::LineEditor
end
def render_finished
- clear_rendered_lines
- render_full_content
- end
-
- def clear_rendered_lines
- Reline::IOGate.move_cursor_up @rendered_screen.cursor_y
- Reline::IOGate.move_cursor_column 0
-
- num_lines = @rendered_screen.lines.size
- return unless num_lines && num_lines >= 1
-
- Reline::IOGate.move_cursor_down num_lines - 1
- (num_lines - 1).times do
- Reline::IOGate.erase_after_cursor
- Reline::IOGate.move_cursor_up 1
- end
- Reline::IOGate.erase_after_cursor
- @rendered_screen.lines = []
- @rendered_screen.cursor_y = 0
- end
-
- def render_full_content
+ render_differential([], 0, 0)
lines = @buffer_of_lines.size.times.map do |i|
line = prompt_list[i] + modified_lines[i]
wrapped_lines, = split_by_width(line, screen_width)
@@ -486,19 +468,13 @@ class Reline::LineEditor
@output.puts lines.map { |l| "#{l}\r\n" }.join
end
- def print_nomultiline_prompt(prompt)
- return unless prompt && !@is_multiline
-
+ def print_nomultiline_prompt
# Readline's test `TestRelineAsReadline#test_readline` requires first output to be prompt, not cursor reset escape sequence.
- @rendered_screen.lines = [[[0, Reline::Unicode.calculate_width(prompt, true), prompt]]]
- @rendered_screen.cursor_y = 0
- @output.write prompt
+ @output.write @prompt if @prompt && !@is_multiline
end
- def render_differential
+ def render
wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
-
- rendered_lines = @rendered_screen.lines
new_lines = wrapped_prompt_and_input_lines.flatten(1)[screen_scroll_top, screen_height].map do |prompt, line|
prompt_width = Reline::Unicode.calculate_width(prompt, true)
[[0, prompt_width, prompt], [prompt_width, Reline::Unicode.calculate_width(line, true), line]]
@@ -516,12 +492,21 @@ class Reline::LineEditor
x_range, y_range = dialog_range dialog, wrapped_cursor_y - screen_scroll_top
y_range.each do |row|
next if row < 0 || row >= screen_height
+
dialog_rows = new_lines[row] ||= []
# index 0 is for prompt, index 1 is for line, index 2.. is for dialog
dialog_rows[index + 2] = [x_range.begin, dialog.width, dialog.contents[row - y_range.begin]]
end
end
+ render_differential new_lines, wrapped_cursor_x, wrapped_cursor_y - screen_scroll_top
+ end
+
+ # Reflects lines to be rendered and new cursor position to the screen
+ # by calculating the difference from the previous render.
+
+ private def render_differential(new_lines, new_cursor_x, new_cursor_y)
+ rendered_lines = @rendered_screen.lines
cursor_y = @rendered_screen.cursor_y
if new_lines != rendered_lines
# Hide cursor while rendering to avoid cursor flickering.
@@ -548,11 +533,14 @@ class Reline::LineEditor
@rendered_screen.lines = new_lines
Reline::IOGate.show_cursor
end
- y = wrapped_cursor_y - screen_scroll_top
- Reline::IOGate.move_cursor_column wrapped_cursor_x
- Reline::IOGate.move_cursor_down y - cursor_y
- @rendered_screen.cursor_y = y
- new_lines.size - y
+ Reline::IOGate.move_cursor_column new_cursor_x
+ Reline::IOGate.move_cursor_down new_cursor_y - cursor_y
+ @rendered_screen.cursor_y = new_cursor_y
+ end
+
+ private def clear_rendered_screen_cache
+ @rendered_screen.lines = []
+ @rendered_screen.cursor_y = 0
end
def upper_space_height(wrapped_cursor_y)
@@ -564,7 +552,7 @@ class Reline::LineEditor
end
def rerender
- render_differential unless @in_pasting
+ render unless @in_pasting
end
class DialogProcScope
@@ -682,10 +670,8 @@ class Reline::LineEditor
@trap_key.each do |t|
@config.add_oneshot_key_binding(t, @name)
end
- elsif @trap_key.is_a?(Array)
+ else
@config.add_oneshot_key_binding(@trap_key, @name)
- elsif @trap_key.is_a?(Integer) or @trap_key.is_a?(Reline::Key)
- @config.add_oneshot_key_binding([@trap_key], @name)
end
end
dialog_render_info
@@ -948,7 +934,8 @@ class Reline::LineEditor
unless @waiting_proc
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
- send(@vi_waiting_operator, byte_pointer_diff)
+ method_obj = method(@vi_waiting_operator)
+ wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
cleanup_waiting
end
else
@@ -1009,7 +996,8 @@ class Reline::LineEditor
if @vi_waiting_operator
byte_pointer_diff = @byte_pointer - old_byte_pointer
@byte_pointer = old_byte_pointer
- send(@vi_waiting_operator, byte_pointer_diff)
+ method_obj = method(@vi_waiting_operator)
+ wrap_method_call(@vi_waiting_operator, method_obj, byte_pointer_diff)
cleanup_waiting
end
@kill_ring.process
@@ -1076,17 +1064,7 @@ class Reline::LineEditor
else # single byte
return if key.char >= 128 # maybe, first byte of multi byte
method_symbol = @config.editing_mode.get_method(key.combined_char)
- if key.with_meta and method_symbol == :ed_unassigned
- if @config.editing_mode_is?(:vi_command, :vi_insert)
- # split ESC + key in vi mode
- method_symbol = @config.editing_mode.get_method("\e".ord)
- process_key("\e".ord, method_symbol)
- method_symbol = @config.editing_mode.get_method(key.char)
- process_key(key.char, method_symbol)
- end
- else
- process_key(key.combined_char, method_symbol)
- end
+ process_key(key.combined_char, method_symbol)
@multibyte_buffer.clear
end
if @config.editing_mode_is?(:vi_command) and @byte_pointer > 0 and @byte_pointer == current_line.bytesize
@@ -1106,6 +1084,7 @@ class Reline::LineEditor
end
def input_key(key)
+ save_old_buffer
@config.reset_oneshot_key_bindings
@dialogs.each do |dialog|
if key.char.instance_of?(Symbol) and key.char == dialog.name
@@ -1114,14 +1093,10 @@ class Reline::LineEditor
end
if key.char.nil?
process_insert(force: true)
- if @first_char
- @eof = true
- end
+ @eof = buffer_empty?
finish
return
end
- old_lines = @buffer_of_lines.dup
- @first_char = false
@completion_occurs = false
if key.char.is_a?(Symbol)
@@ -1129,17 +1104,23 @@ class Reline::LineEditor
else
normal_char(key)
end
+
+ @prev_action_state, @next_action_state = @next_action_state, NullActionState
+
unless @completion_occurs
@completion_state = CompletionState::NORMAL
@completion_journey_state = nil
end
+ push_input_lines unless @undoing
+ @undoing = false
+
if @in_pasting
clear_dialogs
return
end
- modified = old_lines != @buffer_of_lines
+ modified = @old_buffer_of_lines != @buffer_of_lines
if !@completion_occurs && modified && !@config.disable_completion && @config.autocompletion
# Auto complete starts only when edited
process_insert(force: true)
@@ -1148,6 +1129,29 @@ class Reline::LineEditor
modified
end
+ def save_old_buffer
+ @old_buffer_of_lines = @buffer_of_lines.dup
+ end
+
+ def push_input_lines
+ if @old_buffer_of_lines == @buffer_of_lines
+ @input_lines[@input_lines_position] = [@buffer_of_lines.dup, @byte_pointer, @line_index]
+ else
+ @input_lines = @input_lines[0..@input_lines_position]
+ @input_lines_position += 1
+ @input_lines.push([@buffer_of_lines.dup, @byte_pointer, @line_index])
+ end
+ trim_input_lines
+ end
+
+ MAX_INPUT_LINES = 100
+ def trim_input_lines
+ if @input_lines.size > MAX_INPUT_LINES
+ @input_lines.shift
+ @input_lines_position -= 1
+ end
+ end
+
def scroll_into_view
_wrapped_cursor_x, wrapped_cursor_y = wrapped_cursor_position
if wrapped_cursor_y < screen_scroll_top
@@ -1224,6 +1228,18 @@ class Reline::LineEditor
process_auto_indent
end
+ def set_current_lines(lines, byte_pointer = nil, line_index = 0)
+ cursor = current_byte_pointer_cursor
+ @buffer_of_lines = lines
+ @line_index = line_index
+ if byte_pointer
+ @byte_pointer = byte_pointer
+ else
+ calculate_nearest_cursor(cursor)
+ end
+ process_auto_indent
+ end
+
def retrieve_completion_block(set_completion_quote_character = false)
if Reline.completer_word_break_characters.empty?
word_break_regexp = nil
@@ -1305,7 +1321,8 @@ class Reline::LineEditor
@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")
end
- def insert_pasted_text(text)
+ def insert_multiline_text(text)
+ save_old_buffer
pre = @buffer_of_lines[@line_index].byteslice(0, @byte_pointer)
post = @buffer_of_lines[@line_index].byteslice(@byte_pointer..)
lines = (pre + text.gsub(/\r\n?/, "\n") + post).split("\n", -1)
@@ -1313,6 +1330,7 @@ class Reline::LineEditor
@buffer_of_lines[@line_index, 1] = lines
@line_index += lines.size - 1
@byte_pointer = @buffer_of_lines[@line_index].bytesize - post.bytesize
+ push_input_lines
end
def insert_text(text)
@@ -1371,6 +1389,10 @@ class Reline::LineEditor
whole_lines.join("\n")
end
+ private def buffer_empty?
+ current_line.empty? and @buffer_of_lines.size == 1
+ end
+
def finished?
@finished
end
@@ -1719,29 +1741,31 @@ class Reline::LineEditor
end
private def ed_search_prev_history(key, arg: 1)
- substr = current_line.byteslice(0, @byte_pointer)
+ substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer)
return if @history_pointer == 0
return if @history_pointer.nil? && substr.empty? && !current_line.empty?
history_range = 0...(@history_pointer || Reline::HISTORY.size)
h_pointer, line_index = search_history(substr, history_range.reverse_each)
return unless h_pointer
- move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
+ move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
arg -= 1
+ set_next_action_state(:search_history, :empty) if substr.empty?
ed_search_prev_history(key, arg: arg) if arg > 0
end
alias_method :history_search_backward, :ed_search_prev_history
private def ed_search_next_history(key, arg: 1)
- substr = current_line.byteslice(0, @byte_pointer)
+ substr = prev_action_state_value(:search_history) == :empty ? '' : current_line.byteslice(0, @byte_pointer)
return if @history_pointer.nil?
history_range = @history_pointer + 1...Reline::HISTORY.size
h_pointer, line_index = search_history(substr, history_range)
return if h_pointer.nil? and not substr.empty?
- move_history(h_pointer, line: line_index || :start, cursor: @byte_pointer)
+ move_history(h_pointer, line: line_index || :start, cursor: substr.empty? ? :end : @byte_pointer)
arg -= 1
+ set_next_action_state(:search_history, :empty) if substr.empty?
ed_search_next_history(key, arg: arg) if arg > 0
end
alias_method :history_search_forward, :ed_search_next_history
@@ -1897,7 +1921,7 @@ class Reline::LineEditor
alias_method :kill_whole_line, :em_kill_line
private def em_delete(key)
- if current_line.empty? and @buffer_of_lines.size == 1 and key == "\C-d".ord
+ if buffer_empty? and key == "\C-d".ord
@eof = true
finish
elsif @byte_pointer < current_line.bytesize
@@ -1942,9 +1966,8 @@ class Reline::LineEditor
private def ed_clear_screen(key)
Reline::IOGate.clear_screen
@screen_size = Reline::IOGate.get_screen_size
- @rendered_screen.lines = []
@rendered_screen.base_y = 0
- @rendered_screen.cursor_y = 0
+ clear_rendered_screen_cache
end
alias_method :clear_screen, :ed_clear_screen
@@ -2219,9 +2242,11 @@ class Reline::LineEditor
line, cut = byteslice!(current_line, @byte_pointer, byte_pointer_diff)
elsif byte_pointer_diff < 0
line, cut = byteslice!(current_line, @byte_pointer + byte_pointer_diff, -byte_pointer_diff)
+ else
+ return
end
copy_for_vi(cut)
- set_current_line(line || '', @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0))
+ set_current_line(line, @byte_pointer + (byte_pointer_diff < 0 ? byte_pointer_diff : 0))
end
private def vi_yank(key, arg: nil)
@@ -2240,13 +2265,14 @@ class Reline::LineEditor
cut = current_line.byteslice(@byte_pointer, byte_pointer_diff)
elsif byte_pointer_diff < 0
cut = current_line.byteslice(@byte_pointer + byte_pointer_diff, -byte_pointer_diff)
+ else
+ return
end
copy_for_vi(cut)
end
private def vi_list_or_eof(key)
- if current_line.empty? and @buffer_of_lines.size == 1
- set_current_line('', 0)
+ if buffer_empty?
@eof = true
finish
else
@@ -2487,4 +2513,36 @@ class Reline::LineEditor
private def vi_editing_mode(key)
@config.editing_mode = :vi_insert
end
+
+ private def undo(_key)
+ @undoing = true
+
+ return if @input_lines_position <= 0
+
+ @input_lines_position -= 1
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
+ end
+
+ private def redo(_key)
+ @undoing = true
+
+ return if @input_lines_position >= @input_lines.size - 1
+
+ @input_lines_position += 1
+ target_lines, target_cursor_x, target_cursor_y = @input_lines[@input_lines_position]
+ set_current_lines(target_lines.dup, target_cursor_x, target_cursor_y)
+ end
+
+ private def prev_action_state_value(type)
+ @prev_action_state[0] == type ? @prev_action_state[1] : nil
+ end
+
+ private def set_next_action_state(type, value)
+ @next_action_state = [type, value]
+ end
+
+ private def re_read_init_file(_key)
+ @config.reload
+ end
end
diff --git a/lib/reline/terminfo.rb b/lib/reline/terminfo.rb
index 6885a0c6be..c2b1f681b4 100644
--- a/lib/reline/terminfo.rb
+++ b/lib/reline/terminfo.rb
@@ -1,4 +1,7 @@
begin
+ # Ignore warning `Add fiddle to your Gemfile or gemspec` in Ruby 3.4.
+ # terminfo.rb and ansi.rb supports fiddle unavailable environment.
+ verbose, $VERBOSE = $VERBOSE, nil
require 'fiddle'
require 'fiddle/import'
rescue LoadError
@@ -7,6 +10,8 @@ rescue LoadError
false
end
end
+ensure
+ $VERBOSE = verbose
end
module Reline::Terminfo
@@ -78,7 +83,7 @@ module Reline::Terminfo
end
def self.setupterm(term, fildes)
- errret_int = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
+ errret_int = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT, Fiddle::RUBY_FREE)
ret = @setupterm.(term, fildes, errret_int)
case ret
when 0 # OK
diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb
index d7460d6d4a..ef239d5e9e 100644
--- a/lib/reline/unicode.rb
+++ b/lib/reline/unicode.rb
@@ -56,51 +56,26 @@ class Reline::Unicode
require 'reline/unicode/east_asian_width'
- HalfwidthDakutenHandakuten = /[\u{FF9E}\u{FF9F}]/
-
- MBCharWidthRE = /
- (?<width_2_1>
- [#{ EscapedChars.map {|c| "\\x%02x" % c.ord }.join }] (?# ^ + char, such as ^M, ^H, ^[, ...)
- )
- | (?<width_3>^\u{2E3B}) (?# THREE-EM DASH)
- | (?<width_0>^\p{M})
- | (?<width_2_2>
- #{ EastAsianWidth::TYPE_F }
- | #{ EastAsianWidth::TYPE_W }
- )
- | (?<width_1>
- #{ EastAsianWidth::TYPE_H }
- | #{ EastAsianWidth::TYPE_NA }
- | #{ EastAsianWidth::TYPE_N }
- )(?!#{ HalfwidthDakutenHandakuten })
- | (?<width_2_3>
- (?: #{ EastAsianWidth::TYPE_H }
- | #{ EastAsianWidth::TYPE_NA }
- | #{ EastAsianWidth::TYPE_N })
- #{ HalfwidthDakutenHandakuten }
- )
- | (?<ambiguous_width>
- #{EastAsianWidth::TYPE_A}
- )
- /x
-
def self.get_mbchar_width(mbchar)
ord = mbchar.ord
- if (0x00 <= ord and ord <= 0x1F) # in EscapedPairs
+ if ord <= 0x1F # in EscapedPairs
return 2
- elsif (0x20 <= ord and ord <= 0x7E) # printable ASCII chars
+ elsif ord <= 0x7E # printable ASCII chars
return 1
end
- m = mbchar.encode(Encoding::UTF_8).match(MBCharWidthRE)
- case
- when m.nil? then 1 # TODO should be U+FFFD � REPLACEMENT CHARACTER
- when m[:width_2_1], m[:width_2_2], m[:width_2_3] then 2
- when m[:width_3] then 3
- when m[:width_0] then 0
- when m[:width_1] then 1
- when m[:ambiguous_width] then Reline.ambiguous_width
+ utf8_mbchar = mbchar.encode(Encoding::UTF_8)
+ ord = utf8_mbchar.ord
+ chunk_index = EastAsianWidth::CHUNK_LAST.bsearch_index { |o| ord <= o }
+ size = EastAsianWidth::CHUNK_WIDTH[chunk_index]
+ if size == -1
+ Reline.ambiguous_width
+ elsif size == 1 && utf8_mbchar.size >= 2
+ second_char_ord = utf8_mbchar[1].ord
+ # Halfwidth Dakuten Handakuten
+ # Only these two character has Letter Modifier category and can be combined in a single grapheme cluster
+ (second_char_ord == 0xFF9E || second_char_ord == 0xFF9F) ? 2 : 1
else
- nil
+ size
end
end
diff --git a/lib/reline/unicode/east_asian_width.rb b/lib/reline/unicode/east_asian_width.rb
index fa16a1bb56..106ca4881a 100644
--- a/lib/reline/unicode/east_asian_width.rb
+++ b/lib/reline/unicode/east_asian_width.rb
@@ -2,1195 +2,1266 @@ class Reline::Unicode::EastAsianWidth
# This is based on EastAsianWidth.txt
# UNICODE_VERSION = '15.1.0'
- # Fullwidth
- TYPE_F = /^[#{ %W(
- \u{3000}
- \u{FF01}-\u{FF60}
- \u{FFE0}-\u{FFE6}
- ).join }]/
-
- # Halfwidth
- TYPE_H = /^[#{ %W(
- \u{20A9}
- \u{FF61}-\u{FFBE}
- \u{FFC2}-\u{FFC7}
- \u{FFCA}-\u{FFCF}
- \u{FFD2}-\u{FFD7}
- \u{FFDA}-\u{FFDC}
- \u{FFE8}-\u{FFEE}
- ).join }]/
-
- # Wide
- TYPE_W = /^[#{ %W(
- \u{1100}-\u{115F}
- \u{231A}-\u{231B}
- \u{2329}-\u{232A}
- \u{23E9}-\u{23EC}
- \u{23F0}
- \u{23F3}
- \u{25FD}-\u{25FE}
- \u{2614}-\u{2615}
- \u{2648}-\u{2653}
- \u{267F}
- \u{2693}
- \u{26A1}
- \u{26AA}-\u{26AB}
- \u{26BD}-\u{26BE}
- \u{26C4}-\u{26C5}
- \u{26CE}
- \u{26D4}
- \u{26EA}
- \u{26F2}-\u{26F3}
- \u{26F5}
- \u{26FA}
- \u{26FD}
- \u{2705}
- \u{270A}-\u{270B}
- \u{2728}
- \u{274C}
- \u{274E}
- \u{2753}-\u{2755}
- \u{2757}
- \u{2795}-\u{2797}
- \u{27B0}
- \u{27BF}
- \u{2B1B}-\u{2B1C}
- \u{2B50}
- \u{2B55}
- \u{2E80}-\u{2E99}
- \u{2E9B}-\u{2EF3}
- \u{2F00}-\u{2FD5}
- \u{2FF0}-\u{2FFF}
- \u{3001}-\u{303E}
- \u{3041}-\u{3096}
- \u{3099}-\u{30FF}
- \u{3105}-\u{312F}
- \u{3131}-\u{318E}
- \u{3190}-\u{31E3}
- \u{31EF}-\u{321E}
- \u{3220}-\u{3247}
- \u{3250}-\u{4DBF}
- \u{4E00}-\u{A48C}
- \u{A490}-\u{A4C6}
- \u{A960}-\u{A97C}
- \u{AC00}-\u{D7A3}
- \u{F900}-\u{FAFF}
- \u{FE10}-\u{FE19}
- \u{FE30}-\u{FE52}
- \u{FE54}-\u{FE66}
- \u{FE68}-\u{FE6B}
- \u{16FE0}-\u{16FE4}
- \u{16FF0}-\u{16FF1}
- \u{17000}-\u{187F7}
- \u{18800}-\u{18CD5}
- \u{18D00}-\u{18D08}
- \u{1AFF0}-\u{1AFF3}
- \u{1AFF5}-\u{1AFFB}
- \u{1AFFD}-\u{1AFFE}
- \u{1B000}-\u{1B122}
- \u{1B132}
- \u{1B150}-\u{1B152}
- \u{1B155}
- \u{1B164}-\u{1B167}
- \u{1B170}-\u{1B2FB}
- \u{1F004}
- \u{1F0CF}
- \u{1F18E}
- \u{1F191}-\u{1F19A}
- \u{1F200}-\u{1F202}
- \u{1F210}-\u{1F23B}
- \u{1F240}-\u{1F248}
- \u{1F250}-\u{1F251}
- \u{1F260}-\u{1F265}
- \u{1F300}-\u{1F320}
- \u{1F32D}-\u{1F335}
- \u{1F337}-\u{1F37C}
- \u{1F37E}-\u{1F393}
- \u{1F3A0}-\u{1F3CA}
- \u{1F3CF}-\u{1F3D3}
- \u{1F3E0}-\u{1F3F0}
- \u{1F3F4}
- \u{1F3F8}-\u{1F43E}
- \u{1F440}
- \u{1F442}-\u{1F4FC}
- \u{1F4FF}-\u{1F53D}
- \u{1F54B}-\u{1F54E}
- \u{1F550}-\u{1F567}
- \u{1F57A}
- \u{1F595}-\u{1F596}
- \u{1F5A4}
- \u{1F5FB}-\u{1F64F}
- \u{1F680}-\u{1F6C5}
- \u{1F6CC}
- \u{1F6D0}-\u{1F6D2}
- \u{1F6D5}-\u{1F6D7}
- \u{1F6DC}-\u{1F6DF}
- \u{1F6EB}-\u{1F6EC}
- \u{1F6F4}-\u{1F6FC}
- \u{1F7E0}-\u{1F7EB}
- \u{1F7F0}
- \u{1F90C}-\u{1F93A}
- \u{1F93C}-\u{1F945}
- \u{1F947}-\u{1F9FF}
- \u{1FA70}-\u{1FA7C}
- \u{1FA80}-\u{1FA88}
- \u{1FA90}-\u{1FABD}
- \u{1FABF}-\u{1FAC5}
- \u{1FACE}-\u{1FADB}
- \u{1FAE0}-\u{1FAE8}
- \u{1FAF0}-\u{1FAF8}
- \u{20000}-\u{2FFFD}
- \u{30000}-\u{3FFFD}
- ).join }]/
-
- # Narrow
- TYPE_NA = /^[#{ %W(
- \u{0020}-\u{007E}
- \u{00A2}-\u{00A3}
- \u{00A5}-\u{00A6}
- \u{00AC}
- \u{00AF}
- \u{27E6}-\u{27ED}
- \u{2985}-\u{2986}
- ).join }]/
-
- # Ambiguous
- TYPE_A = /^[#{ %W(
- \u{00A1}
- \u{00A4}
- \u{00A7}-\u{00A8}
- \u{00AA}
- \u{00AD}-\u{00AE}
- \u{00B0}-\u{00B4}
- \u{00B6}-\u{00BA}
- \u{00BC}-\u{00BF}
- \u{00C6}
- \u{00D0}
- \u{00D7}-\u{00D8}
- \u{00DE}-\u{00E1}
- \u{00E6}
- \u{00E8}-\u{00EA}
- \u{00EC}-\u{00ED}
- \u{00F0}
- \u{00F2}-\u{00F3}
- \u{00F7}-\u{00FA}
- \u{00FC}
- \u{00FE}
- \u{0101}
- \u{0111}
- \u{0113}
- \u{011B}
- \u{0126}-\u{0127}
- \u{012B}
- \u{0131}-\u{0133}
- \u{0138}
- \u{013F}-\u{0142}
- \u{0144}
- \u{0148}-\u{014B}
- \u{014D}
- \u{0152}-\u{0153}
- \u{0166}-\u{0167}
- \u{016B}
- \u{01CE}
- \u{01D0}
- \u{01D2}
- \u{01D4}
- \u{01D6}
- \u{01D8}
- \u{01DA}
- \u{01DC}
- \u{0251}
- \u{0261}
- \u{02C4}
- \u{02C7}
- \u{02C9}-\u{02CB}
- \u{02CD}
- \u{02D0}
- \u{02D8}-\u{02DB}
- \u{02DD}
- \u{02DF}
- \u{0300}-\u{036F}
- \u{0391}-\u{03A1}
- \u{03A3}-\u{03A9}
- \u{03B1}-\u{03C1}
- \u{03C3}-\u{03C9}
- \u{0401}
- \u{0410}-\u{044F}
- \u{0451}
- \u{2010}
- \u{2013}-\u{2016}
- \u{2018}-\u{2019}
- \u{201C}-\u{201D}
- \u{2020}-\u{2022}
- \u{2024}-\u{2027}
- \u{2030}
- \u{2032}-\u{2033}
- \u{2035}
- \u{203B}
- \u{203E}
- \u{2074}
- \u{207F}
- \u{2081}-\u{2084}
- \u{20AC}
- \u{2103}
- \u{2105}
- \u{2109}
- \u{2113}
- \u{2116}
- \u{2121}-\u{2122}
- \u{2126}
- \u{212B}
- \u{2153}-\u{2154}
- \u{215B}-\u{215E}
- \u{2160}-\u{216B}
- \u{2170}-\u{2179}
- \u{2189}
- \u{2190}-\u{2199}
- \u{21B8}-\u{21B9}
- \u{21D2}
- \u{21D4}
- \u{21E7}
- \u{2200}
- \u{2202}-\u{2203}
- \u{2207}-\u{2208}
- \u{220B}
- \u{220F}
- \u{2211}
- \u{2215}
- \u{221A}
- \u{221D}-\u{2220}
- \u{2223}
- \u{2225}
- \u{2227}-\u{222C}
- \u{222E}
- \u{2234}-\u{2237}
- \u{223C}-\u{223D}
- \u{2248}
- \u{224C}
- \u{2252}
- \u{2260}-\u{2261}
- \u{2264}-\u{2267}
- \u{226A}-\u{226B}
- \u{226E}-\u{226F}
- \u{2282}-\u{2283}
- \u{2286}-\u{2287}
- \u{2295}
- \u{2299}
- \u{22A5}
- \u{22BF}
- \u{2312}
- \u{2460}-\u{24E9}
- \u{24EB}-\u{254B}
- \u{2550}-\u{2573}
- \u{2580}-\u{258F}
- \u{2592}-\u{2595}
- \u{25A0}-\u{25A1}
- \u{25A3}-\u{25A9}
- \u{25B2}-\u{25B3}
- \u{25B6}-\u{25B7}
- \u{25BC}-\u{25BD}
- \u{25C0}-\u{25C1}
- \u{25C6}-\u{25C8}
- \u{25CB}
- \u{25CE}-\u{25D1}
- \u{25E2}-\u{25E5}
- \u{25EF}
- \u{2605}-\u{2606}
- \u{2609}
- \u{260E}-\u{260F}
- \u{261C}
- \u{261E}
- \u{2640}
- \u{2642}
- \u{2660}-\u{2661}
- \u{2663}-\u{2665}
- \u{2667}-\u{266A}
- \u{266C}-\u{266D}
- \u{266F}
- \u{269E}-\u{269F}
- \u{26BF}
- \u{26C6}-\u{26CD}
- \u{26CF}-\u{26D3}
- \u{26D5}-\u{26E1}
- \u{26E3}
- \u{26E8}-\u{26E9}
- \u{26EB}-\u{26F1}
- \u{26F4}
- \u{26F6}-\u{26F9}
- \u{26FB}-\u{26FC}
- \u{26FE}-\u{26FF}
- \u{273D}
- \u{2776}-\u{277F}
- \u{2B56}-\u{2B59}
- \u{3248}-\u{324F}
- \u{E000}-\u{F8FF}
- \u{FE00}-\u{FE0F}
- \u{FFFD}
- \u{1F100}-\u{1F10A}
- \u{1F110}-\u{1F12D}
- \u{1F130}-\u{1F169}
- \u{1F170}-\u{1F18D}
- \u{1F18F}-\u{1F190}
- \u{1F19B}-\u{1F1AC}
- \u{E0100}-\u{E01EF}
- \u{F0000}-\u{FFFFD}
- \u{100000}-\u{10FFFD}
- ).join }]/
-
- # Neutral
- TYPE_N = /^[#{ %W(
- \u{0000}-\u{001F}
- \u{007F}-\u{00A0}
- \u{00A9}
- \u{00AB}
- \u{00B5}
- \u{00BB}
- \u{00C0}-\u{00C5}
- \u{00C7}-\u{00CF}
- \u{00D1}-\u{00D6}
- \u{00D9}-\u{00DD}
- \u{00E2}-\u{00E5}
- \u{00E7}
- \u{00EB}
- \u{00EE}-\u{00EF}
- \u{00F1}
- \u{00F4}-\u{00F6}
- \u{00FB}
- \u{00FD}
- \u{00FF}-\u{0100}
- \u{0102}-\u{0110}
- \u{0112}
- \u{0114}-\u{011A}
- \u{011C}-\u{0125}
- \u{0128}-\u{012A}
- \u{012C}-\u{0130}
- \u{0134}-\u{0137}
- \u{0139}-\u{013E}
- \u{0143}
- \u{0145}-\u{0147}
- \u{014C}
- \u{014E}-\u{0151}
- \u{0154}-\u{0165}
- \u{0168}-\u{016A}
- \u{016C}-\u{01CD}
- \u{01CF}
- \u{01D1}
- \u{01D3}
- \u{01D5}
- \u{01D7}
- \u{01D9}
- \u{01DB}
- \u{01DD}-\u{0250}
- \u{0252}-\u{0260}
- \u{0262}-\u{02C3}
- \u{02C5}-\u{02C6}
- \u{02C8}
- \u{02CC}
- \u{02CE}-\u{02CF}
- \u{02D1}-\u{02D7}
- \u{02DC}
- \u{02DE}
- \u{02E0}-\u{02FF}
- \u{0370}-\u{0377}
- \u{037A}-\u{037F}
- \u{0384}-\u{038A}
- \u{038C}
- \u{038E}-\u{0390}
- \u{03AA}-\u{03B0}
- \u{03C2}
- \u{03CA}-\u{0400}
- \u{0402}-\u{040F}
- \u{0450}
- \u{0452}-\u{052F}
- \u{0531}-\u{0556}
- \u{0559}-\u{058A}
- \u{058D}-\u{058F}
- \u{0591}-\u{05C7}
- \u{05D0}-\u{05EA}
- \u{05EF}-\u{05F4}
- \u{0600}-\u{070D}
- \u{070F}-\u{074A}
- \u{074D}-\u{07B1}
- \u{07C0}-\u{07FA}
- \u{07FD}-\u{082D}
- \u{0830}-\u{083E}
- \u{0840}-\u{085B}
- \u{085E}
- \u{0860}-\u{086A}
- \u{0870}-\u{088E}
- \u{0890}-\u{0891}
- \u{0898}-\u{0983}
- \u{0985}-\u{098C}
- \u{098F}-\u{0990}
- \u{0993}-\u{09A8}
- \u{09AA}-\u{09B0}
- \u{09B2}
- \u{09B6}-\u{09B9}
- \u{09BC}-\u{09C4}
- \u{09C7}-\u{09C8}
- \u{09CB}-\u{09CE}
- \u{09D7}
- \u{09DC}-\u{09DD}
- \u{09DF}-\u{09E3}
- \u{09E6}-\u{09FE}
- \u{0A01}-\u{0A03}
- \u{0A05}-\u{0A0A}
- \u{0A0F}-\u{0A10}
- \u{0A13}-\u{0A28}
- \u{0A2A}-\u{0A30}
- \u{0A32}-\u{0A33}
- \u{0A35}-\u{0A36}
- \u{0A38}-\u{0A39}
- \u{0A3C}
- \u{0A3E}-\u{0A42}
- \u{0A47}-\u{0A48}
- \u{0A4B}-\u{0A4D}
- \u{0A51}
- \u{0A59}-\u{0A5C}
- \u{0A5E}
- \u{0A66}-\u{0A76}
- \u{0A81}-\u{0A83}
- \u{0A85}-\u{0A8D}
- \u{0A8F}-\u{0A91}
- \u{0A93}-\u{0AA8}
- \u{0AAA}-\u{0AB0}
- \u{0AB2}-\u{0AB3}
- \u{0AB5}-\u{0AB9}
- \u{0ABC}-\u{0AC5}
- \u{0AC7}-\u{0AC9}
- \u{0ACB}-\u{0ACD}
- \u{0AD0}
- \u{0AE0}-\u{0AE3}
- \u{0AE6}-\u{0AF1}
- \u{0AF9}-\u{0AFF}
- \u{0B01}-\u{0B03}
- \u{0B05}-\u{0B0C}
- \u{0B0F}-\u{0B10}
- \u{0B13}-\u{0B28}
- \u{0B2A}-\u{0B30}
- \u{0B32}-\u{0B33}
- \u{0B35}-\u{0B39}
- \u{0B3C}-\u{0B44}
- \u{0B47}-\u{0B48}
- \u{0B4B}-\u{0B4D}
- \u{0B55}-\u{0B57}
- \u{0B5C}-\u{0B5D}
- \u{0B5F}-\u{0B63}
- \u{0B66}-\u{0B77}
- \u{0B82}-\u{0B83}
- \u{0B85}-\u{0B8A}
- \u{0B8E}-\u{0B90}
- \u{0B92}-\u{0B95}
- \u{0B99}-\u{0B9A}
- \u{0B9C}
- \u{0B9E}-\u{0B9F}
- \u{0BA3}-\u{0BA4}
- \u{0BA8}-\u{0BAA}
- \u{0BAE}-\u{0BB9}
- \u{0BBE}-\u{0BC2}
- \u{0BC6}-\u{0BC8}
- \u{0BCA}-\u{0BCD}
- \u{0BD0}
- \u{0BD7}
- \u{0BE6}-\u{0BFA}
- \u{0C00}-\u{0C0C}
- \u{0C0E}-\u{0C10}
- \u{0C12}-\u{0C28}
- \u{0C2A}-\u{0C39}
- \u{0C3C}-\u{0C44}
- \u{0C46}-\u{0C48}
- \u{0C4A}-\u{0C4D}
- \u{0C55}-\u{0C56}
- \u{0C58}-\u{0C5A}
- \u{0C5D}
- \u{0C60}-\u{0C63}
- \u{0C66}-\u{0C6F}
- \u{0C77}-\u{0C8C}
- \u{0C8E}-\u{0C90}
- \u{0C92}-\u{0CA8}
- \u{0CAA}-\u{0CB3}
- \u{0CB5}-\u{0CB9}
- \u{0CBC}-\u{0CC4}
- \u{0CC6}-\u{0CC8}
- \u{0CCA}-\u{0CCD}
- \u{0CD5}-\u{0CD6}
- \u{0CDD}-\u{0CDE}
- \u{0CE0}-\u{0CE3}
- \u{0CE6}-\u{0CEF}
- \u{0CF1}-\u{0CF3}
- \u{0D00}-\u{0D0C}
- \u{0D0E}-\u{0D10}
- \u{0D12}-\u{0D44}
- \u{0D46}-\u{0D48}
- \u{0D4A}-\u{0D4F}
- \u{0D54}-\u{0D63}
- \u{0D66}-\u{0D7F}
- \u{0D81}-\u{0D83}
- \u{0D85}-\u{0D96}
- \u{0D9A}-\u{0DB1}
- \u{0DB3}-\u{0DBB}
- \u{0DBD}
- \u{0DC0}-\u{0DC6}
- \u{0DCA}
- \u{0DCF}-\u{0DD4}
- \u{0DD6}
- \u{0DD8}-\u{0DDF}
- \u{0DE6}-\u{0DEF}
- \u{0DF2}-\u{0DF4}
- \u{0E01}-\u{0E3A}
- \u{0E3F}-\u{0E5B}
- \u{0E81}-\u{0E82}
- \u{0E84}
- \u{0E86}-\u{0E8A}
- \u{0E8C}-\u{0EA3}
- \u{0EA5}
- \u{0EA7}-\u{0EBD}
- \u{0EC0}-\u{0EC4}
- \u{0EC6}
- \u{0EC8}-\u{0ECE}
- \u{0ED0}-\u{0ED9}
- \u{0EDC}-\u{0EDF}
- \u{0F00}-\u{0F47}
- \u{0F49}-\u{0F6C}
- \u{0F71}-\u{0F97}
- \u{0F99}-\u{0FBC}
- \u{0FBE}-\u{0FCC}
- \u{0FCE}-\u{0FDA}
- \u{1000}-\u{10C5}
- \u{10C7}
- \u{10CD}
- \u{10D0}-\u{10FF}
- \u{1160}-\u{1248}
- \u{124A}-\u{124D}
- \u{1250}-\u{1256}
- \u{1258}
- \u{125A}-\u{125D}
- \u{1260}-\u{1288}
- \u{128A}-\u{128D}
- \u{1290}-\u{12B0}
- \u{12B2}-\u{12B5}
- \u{12B8}-\u{12BE}
- \u{12C0}
- \u{12C2}-\u{12C5}
- \u{12C8}-\u{12D6}
- \u{12D8}-\u{1310}
- \u{1312}-\u{1315}
- \u{1318}-\u{135A}
- \u{135D}-\u{137C}
- \u{1380}-\u{1399}
- \u{13A0}-\u{13F5}
- \u{13F8}-\u{13FD}
- \u{1400}-\u{169C}
- \u{16A0}-\u{16F8}
- \u{1700}-\u{1715}
- \u{171F}-\u{1736}
- \u{1740}-\u{1753}
- \u{1760}-\u{176C}
- \u{176E}-\u{1770}
- \u{1772}-\u{1773}
- \u{1780}-\u{17DD}
- \u{17E0}-\u{17E9}
- \u{17F0}-\u{17F9}
- \u{1800}-\u{1819}
- \u{1820}-\u{1878}
- \u{1880}-\u{18AA}
- \u{18B0}-\u{18F5}
- \u{1900}-\u{191E}
- \u{1920}-\u{192B}
- \u{1930}-\u{193B}
- \u{1940}
- \u{1944}-\u{196D}
- \u{1970}-\u{1974}
- \u{1980}-\u{19AB}
- \u{19B0}-\u{19C9}
- \u{19D0}-\u{19DA}
- \u{19DE}-\u{1A1B}
- \u{1A1E}-\u{1A5E}
- \u{1A60}-\u{1A7C}
- \u{1A7F}-\u{1A89}
- \u{1A90}-\u{1A99}
- \u{1AA0}-\u{1AAD}
- \u{1AB0}-\u{1ACE}
- \u{1B00}-\u{1B4C}
- \u{1B50}-\u{1B7E}
- \u{1B80}-\u{1BF3}
- \u{1BFC}-\u{1C37}
- \u{1C3B}-\u{1C49}
- \u{1C4D}-\u{1C88}
- \u{1C90}-\u{1CBA}
- \u{1CBD}-\u{1CC7}
- \u{1CD0}-\u{1CFA}
- \u{1D00}-\u{1F15}
- \u{1F18}-\u{1F1D}
- \u{1F20}-\u{1F45}
- \u{1F48}-\u{1F4D}
- \u{1F50}-\u{1F57}
- \u{1F59}
- \u{1F5B}
- \u{1F5D}
- \u{1F5F}-\u{1F7D}
- \u{1F80}-\u{1FB4}
- \u{1FB6}-\u{1FC4}
- \u{1FC6}-\u{1FD3}
- \u{1FD6}-\u{1FDB}
- \u{1FDD}-\u{1FEF}
- \u{1FF2}-\u{1FF4}
- \u{1FF6}-\u{1FFE}
- \u{2000}-\u{200F}
- \u{2011}-\u{2012}
- \u{2017}
- \u{201A}-\u{201B}
- \u{201E}-\u{201F}
- \u{2023}
- \u{2028}-\u{202F}
- \u{2031}
- \u{2034}
- \u{2036}-\u{203A}
- \u{203C}-\u{203D}
- \u{203F}-\u{2064}
- \u{2066}-\u{2071}
- \u{2075}-\u{207E}
- \u{2080}
- \u{2085}-\u{208E}
- \u{2090}-\u{209C}
- \u{20A0}-\u{20A8}
- \u{20AA}-\u{20AB}
- \u{20AD}-\u{20C0}
- \u{20D0}-\u{20F0}
- \u{2100}-\u{2102}
- \u{2104}
- \u{2106}-\u{2108}
- \u{210A}-\u{2112}
- \u{2114}-\u{2115}
- \u{2117}-\u{2120}
- \u{2123}-\u{2125}
- \u{2127}-\u{212A}
- \u{212C}-\u{2152}
- \u{2155}-\u{215A}
- \u{215F}
- \u{216C}-\u{216F}
- \u{217A}-\u{2188}
- \u{218A}-\u{218B}
- \u{219A}-\u{21B7}
- \u{21BA}-\u{21D1}
- \u{21D3}
- \u{21D5}-\u{21E6}
- \u{21E8}-\u{21FF}
- \u{2201}
- \u{2204}-\u{2206}
- \u{2209}-\u{220A}
- \u{220C}-\u{220E}
- \u{2210}
- \u{2212}-\u{2214}
- \u{2216}-\u{2219}
- \u{221B}-\u{221C}
- \u{2221}-\u{2222}
- \u{2224}
- \u{2226}
- \u{222D}
- \u{222F}-\u{2233}
- \u{2238}-\u{223B}
- \u{223E}-\u{2247}
- \u{2249}-\u{224B}
- \u{224D}-\u{2251}
- \u{2253}-\u{225F}
- \u{2262}-\u{2263}
- \u{2268}-\u{2269}
- \u{226C}-\u{226D}
- \u{2270}-\u{2281}
- \u{2284}-\u{2285}
- \u{2288}-\u{2294}
- \u{2296}-\u{2298}
- \u{229A}-\u{22A4}
- \u{22A6}-\u{22BE}
- \u{22C0}-\u{2311}
- \u{2313}-\u{2319}
- \u{231C}-\u{2328}
- \u{232B}-\u{23E8}
- \u{23ED}-\u{23EF}
- \u{23F1}-\u{23F2}
- \u{23F4}-\u{2426}
- \u{2440}-\u{244A}
- \u{24EA}
- \u{254C}-\u{254F}
- \u{2574}-\u{257F}
- \u{2590}-\u{2591}
- \u{2596}-\u{259F}
- \u{25A2}
- \u{25AA}-\u{25B1}
- \u{25B4}-\u{25B5}
- \u{25B8}-\u{25BB}
- \u{25BE}-\u{25BF}
- \u{25C2}-\u{25C5}
- \u{25C9}-\u{25CA}
- \u{25CC}-\u{25CD}
- \u{25D2}-\u{25E1}
- \u{25E6}-\u{25EE}
- \u{25F0}-\u{25FC}
- \u{25FF}-\u{2604}
- \u{2607}-\u{2608}
- \u{260A}-\u{260D}
- \u{2610}-\u{2613}
- \u{2616}-\u{261B}
- \u{261D}
- \u{261F}-\u{263F}
- \u{2641}
- \u{2643}-\u{2647}
- \u{2654}-\u{265F}
- \u{2662}
- \u{2666}
- \u{266B}
- \u{266E}
- \u{2670}-\u{267E}
- \u{2680}-\u{2692}
- \u{2694}-\u{269D}
- \u{26A0}
- \u{26A2}-\u{26A9}
- \u{26AC}-\u{26BC}
- \u{26C0}-\u{26C3}
- \u{26E2}
- \u{26E4}-\u{26E7}
- \u{2700}-\u{2704}
- \u{2706}-\u{2709}
- \u{270C}-\u{2727}
- \u{2729}-\u{273C}
- \u{273E}-\u{274B}
- \u{274D}
- \u{274F}-\u{2752}
- \u{2756}
- \u{2758}-\u{2775}
- \u{2780}-\u{2794}
- \u{2798}-\u{27AF}
- \u{27B1}-\u{27BE}
- \u{27C0}-\u{27E5}
- \u{27EE}-\u{2984}
- \u{2987}-\u{2B1A}
- \u{2B1D}-\u{2B4F}
- \u{2B51}-\u{2B54}
- \u{2B5A}-\u{2B73}
- \u{2B76}-\u{2B95}
- \u{2B97}-\u{2CF3}
- \u{2CF9}-\u{2D25}
- \u{2D27}
- \u{2D2D}
- \u{2D30}-\u{2D67}
- \u{2D6F}-\u{2D70}
- \u{2D7F}-\u{2D96}
- \u{2DA0}-\u{2DA6}
- \u{2DA8}-\u{2DAE}
- \u{2DB0}-\u{2DB6}
- \u{2DB8}-\u{2DBE}
- \u{2DC0}-\u{2DC6}
- \u{2DC8}-\u{2DCE}
- \u{2DD0}-\u{2DD6}
- \u{2DD8}-\u{2DDE}
- \u{2DE0}-\u{2E5D}
- \u{303F}
- \u{4DC0}-\u{4DFF}
- \u{A4D0}-\u{A62B}
- \u{A640}-\u{A6F7}
- \u{A700}-\u{A7CA}
- \u{A7D0}-\u{A7D1}
- \u{A7D3}
- \u{A7D5}-\u{A7D9}
- \u{A7F2}-\u{A82C}
- \u{A830}-\u{A839}
- \u{A840}-\u{A877}
- \u{A880}-\u{A8C5}
- \u{A8CE}-\u{A8D9}
- \u{A8E0}-\u{A953}
- \u{A95F}
- \u{A980}-\u{A9CD}
- \u{A9CF}-\u{A9D9}
- \u{A9DE}-\u{A9FE}
- \u{AA00}-\u{AA36}
- \u{AA40}-\u{AA4D}
- \u{AA50}-\u{AA59}
- \u{AA5C}-\u{AAC2}
- \u{AADB}-\u{AAF6}
- \u{AB01}-\u{AB06}
- \u{AB09}-\u{AB0E}
- \u{AB11}-\u{AB16}
- \u{AB20}-\u{AB26}
- \u{AB28}-\u{AB2E}
- \u{AB30}-\u{AB6B}
- \u{AB70}-\u{ABED}
- \u{ABF0}-\u{ABF9}
- \u{D7B0}-\u{D7C6}
- \u{D7CB}-\u{D7FB}
- \u{FB00}-\u{FB06}
- \u{FB13}-\u{FB17}
- \u{FB1D}-\u{FB36}
- \u{FB38}-\u{FB3C}
- \u{FB3E}
- \u{FB40}-\u{FB41}
- \u{FB43}-\u{FB44}
- \u{FB46}-\u{FBC2}
- \u{FBD3}-\u{FD8F}
- \u{FD92}-\u{FDC7}
- \u{FDCF}
- \u{FDF0}-\u{FDFF}
- \u{FE20}-\u{FE2F}
- \u{FE70}-\u{FE74}
- \u{FE76}-\u{FEFC}
- \u{FEFF}
- \u{FFF9}-\u{FFFC}
- \u{10000}-\u{1000B}
- \u{1000D}-\u{10026}
- \u{10028}-\u{1003A}
- \u{1003C}-\u{1003D}
- \u{1003F}-\u{1004D}
- \u{10050}-\u{1005D}
- \u{10080}-\u{100FA}
- \u{10100}-\u{10102}
- \u{10107}-\u{10133}
- \u{10137}-\u{1018E}
- \u{10190}-\u{1019C}
- \u{101A0}
- \u{101D0}-\u{101FD}
- \u{10280}-\u{1029C}
- \u{102A0}-\u{102D0}
- \u{102E0}-\u{102FB}
- \u{10300}-\u{10323}
- \u{1032D}-\u{1034A}
- \u{10350}-\u{1037A}
- \u{10380}-\u{1039D}
- \u{1039F}-\u{103C3}
- \u{103C8}-\u{103D5}
- \u{10400}-\u{1049D}
- \u{104A0}-\u{104A9}
- \u{104B0}-\u{104D3}
- \u{104D8}-\u{104FB}
- \u{10500}-\u{10527}
- \u{10530}-\u{10563}
- \u{1056F}-\u{1057A}
- \u{1057C}-\u{1058A}
- \u{1058C}-\u{10592}
- \u{10594}-\u{10595}
- \u{10597}-\u{105A1}
- \u{105A3}-\u{105B1}
- \u{105B3}-\u{105B9}
- \u{105BB}-\u{105BC}
- \u{10600}-\u{10736}
- \u{10740}-\u{10755}
- \u{10760}-\u{10767}
- \u{10780}-\u{10785}
- \u{10787}-\u{107B0}
- \u{107B2}-\u{107BA}
- \u{10800}-\u{10805}
- \u{10808}
- \u{1080A}-\u{10835}
- \u{10837}-\u{10838}
- \u{1083C}
- \u{1083F}-\u{10855}
- \u{10857}-\u{1089E}
- \u{108A7}-\u{108AF}
- \u{108E0}-\u{108F2}
- \u{108F4}-\u{108F5}
- \u{108FB}-\u{1091B}
- \u{1091F}-\u{10939}
- \u{1093F}
- \u{10980}-\u{109B7}
- \u{109BC}-\u{109CF}
- \u{109D2}-\u{10A03}
- \u{10A05}-\u{10A06}
- \u{10A0C}-\u{10A13}
- \u{10A15}-\u{10A17}
- \u{10A19}-\u{10A35}
- \u{10A38}-\u{10A3A}
- \u{10A3F}-\u{10A48}
- \u{10A50}-\u{10A58}
- \u{10A60}-\u{10A9F}
- \u{10AC0}-\u{10AE6}
- \u{10AEB}-\u{10AF6}
- \u{10B00}-\u{10B35}
- \u{10B39}-\u{10B55}
- \u{10B58}-\u{10B72}
- \u{10B78}-\u{10B91}
- \u{10B99}-\u{10B9C}
- \u{10BA9}-\u{10BAF}
- \u{10C00}-\u{10C48}
- \u{10C80}-\u{10CB2}
- \u{10CC0}-\u{10CF2}
- \u{10CFA}-\u{10D27}
- \u{10D30}-\u{10D39}
- \u{10E60}-\u{10E7E}
- \u{10E80}-\u{10EA9}
- \u{10EAB}-\u{10EAD}
- \u{10EB0}-\u{10EB1}
- \u{10EFD}-\u{10F27}
- \u{10F30}-\u{10F59}
- \u{10F70}-\u{10F89}
- \u{10FB0}-\u{10FCB}
- \u{10FE0}-\u{10FF6}
- \u{11000}-\u{1104D}
- \u{11052}-\u{11075}
- \u{1107F}-\u{110C2}
- \u{110CD}
- \u{110D0}-\u{110E8}
- \u{110F0}-\u{110F9}
- \u{11100}-\u{11134}
- \u{11136}-\u{11147}
- \u{11150}-\u{11176}
- \u{11180}-\u{111DF}
- \u{111E1}-\u{111F4}
- \u{11200}-\u{11211}
- \u{11213}-\u{11241}
- \u{11280}-\u{11286}
- \u{11288}
- \u{1128A}-\u{1128D}
- \u{1128F}-\u{1129D}
- \u{1129F}-\u{112A9}
- \u{112B0}-\u{112EA}
- \u{112F0}-\u{112F9}
- \u{11300}-\u{11303}
- \u{11305}-\u{1130C}
- \u{1130F}-\u{11310}
- \u{11313}-\u{11328}
- \u{1132A}-\u{11330}
- \u{11332}-\u{11333}
- \u{11335}-\u{11339}
- \u{1133B}-\u{11344}
- \u{11347}-\u{11348}
- \u{1134B}-\u{1134D}
- \u{11350}
- \u{11357}
- \u{1135D}-\u{11363}
- \u{11366}-\u{1136C}
- \u{11370}-\u{11374}
- \u{11400}-\u{1145B}
- \u{1145D}-\u{11461}
- \u{11480}-\u{114C7}
- \u{114D0}-\u{114D9}
- \u{11580}-\u{115B5}
- \u{115B8}-\u{115DD}
- \u{11600}-\u{11644}
- \u{11650}-\u{11659}
- \u{11660}-\u{1166C}
- \u{11680}-\u{116B9}
- \u{116C0}-\u{116C9}
- \u{11700}-\u{1171A}
- \u{1171D}-\u{1172B}
- \u{11730}-\u{11746}
- \u{11800}-\u{1183B}
- \u{118A0}-\u{118F2}
- \u{118FF}-\u{11906}
- \u{11909}
- \u{1190C}-\u{11913}
- \u{11915}-\u{11916}
- \u{11918}-\u{11935}
- \u{11937}-\u{11938}
- \u{1193B}-\u{11946}
- \u{11950}-\u{11959}
- \u{119A0}-\u{119A7}
- \u{119AA}-\u{119D7}
- \u{119DA}-\u{119E4}
- \u{11A00}-\u{11A47}
- \u{11A50}-\u{11AA2}
- \u{11AB0}-\u{11AF8}
- \u{11B00}-\u{11B09}
- \u{11C00}-\u{11C08}
- \u{11C0A}-\u{11C36}
- \u{11C38}-\u{11C45}
- \u{11C50}-\u{11C6C}
- \u{11C70}-\u{11C8F}
- \u{11C92}-\u{11CA7}
- \u{11CA9}-\u{11CB6}
- \u{11D00}-\u{11D06}
- \u{11D08}-\u{11D09}
- \u{11D0B}-\u{11D36}
- \u{11D3A}
- \u{11D3C}-\u{11D3D}
- \u{11D3F}-\u{11D47}
- \u{11D50}-\u{11D59}
- \u{11D60}-\u{11D65}
- \u{11D67}-\u{11D68}
- \u{11D6A}-\u{11D8E}
- \u{11D90}-\u{11D91}
- \u{11D93}-\u{11D98}
- \u{11DA0}-\u{11DA9}
- \u{11EE0}-\u{11EF8}
- \u{11F00}-\u{11F10}
- \u{11F12}-\u{11F3A}
- \u{11F3E}-\u{11F59}
- \u{11FB0}
- \u{11FC0}-\u{11FF1}
- \u{11FFF}-\u{12399}
- \u{12400}-\u{1246E}
- \u{12470}-\u{12474}
- \u{12480}-\u{12543}
- \u{12F90}-\u{12FF2}
- \u{13000}-\u{13455}
- \u{14400}-\u{14646}
- \u{16800}-\u{16A38}
- \u{16A40}-\u{16A5E}
- \u{16A60}-\u{16A69}
- \u{16A6E}-\u{16ABE}
- \u{16AC0}-\u{16AC9}
- \u{16AD0}-\u{16AED}
- \u{16AF0}-\u{16AF5}
- \u{16B00}-\u{16B45}
- \u{16B50}-\u{16B59}
- \u{16B5B}-\u{16B61}
- \u{16B63}-\u{16B77}
- \u{16B7D}-\u{16B8F}
- \u{16E40}-\u{16E9A}
- \u{16F00}-\u{16F4A}
- \u{16F4F}-\u{16F87}
- \u{16F8F}-\u{16F9F}
- \u{1BC00}-\u{1BC6A}
- \u{1BC70}-\u{1BC7C}
- \u{1BC80}-\u{1BC88}
- \u{1BC90}-\u{1BC99}
- \u{1BC9C}-\u{1BCA3}
- \u{1CF00}-\u{1CF2D}
- \u{1CF30}-\u{1CF46}
- \u{1CF50}-\u{1CFC3}
- \u{1D000}-\u{1D0F5}
- \u{1D100}-\u{1D126}
- \u{1D129}-\u{1D1EA}
- \u{1D200}-\u{1D245}
- \u{1D2C0}-\u{1D2D3}
- \u{1D2E0}-\u{1D2F3}
- \u{1D300}-\u{1D356}
- \u{1D360}-\u{1D378}
- \u{1D400}-\u{1D454}
- \u{1D456}-\u{1D49C}
- \u{1D49E}-\u{1D49F}
- \u{1D4A2}
- \u{1D4A5}-\u{1D4A6}
- \u{1D4A9}-\u{1D4AC}
- \u{1D4AE}-\u{1D4B9}
- \u{1D4BB}
- \u{1D4BD}-\u{1D4C3}
- \u{1D4C5}-\u{1D505}
- \u{1D507}-\u{1D50A}
- \u{1D50D}-\u{1D514}
- \u{1D516}-\u{1D51C}
- \u{1D51E}-\u{1D539}
- \u{1D53B}-\u{1D53E}
- \u{1D540}-\u{1D544}
- \u{1D546}
- \u{1D54A}-\u{1D550}
- \u{1D552}-\u{1D6A5}
- \u{1D6A8}-\u{1D7CB}
- \u{1D7CE}-\u{1DA8B}
- \u{1DA9B}-\u{1DA9F}
- \u{1DAA1}-\u{1DAAF}
- \u{1DF00}-\u{1DF1E}
- \u{1DF25}-\u{1DF2A}
- \u{1E000}-\u{1E006}
- \u{1E008}-\u{1E018}
- \u{1E01B}-\u{1E021}
- \u{1E023}-\u{1E024}
- \u{1E026}-\u{1E02A}
- \u{1E030}-\u{1E06D}
- \u{1E08F}
- \u{1E100}-\u{1E12C}
- \u{1E130}-\u{1E13D}
- \u{1E140}-\u{1E149}
- \u{1E14E}-\u{1E14F}
- \u{1E290}-\u{1E2AE}
- \u{1E2C0}-\u{1E2F9}
- \u{1E2FF}
- \u{1E4D0}-\u{1E4F9}
- \u{1E7E0}-\u{1E7E6}
- \u{1E7E8}-\u{1E7EB}
- \u{1E7ED}-\u{1E7EE}
- \u{1E7F0}-\u{1E7FE}
- \u{1E800}-\u{1E8C4}
- \u{1E8C7}-\u{1E8D6}
- \u{1E900}-\u{1E94B}
- \u{1E950}-\u{1E959}
- \u{1E95E}-\u{1E95F}
- \u{1EC71}-\u{1ECB4}
- \u{1ED01}-\u{1ED3D}
- \u{1EE00}-\u{1EE03}
- \u{1EE05}-\u{1EE1F}
- \u{1EE21}-\u{1EE22}
- \u{1EE24}
- \u{1EE27}
- \u{1EE29}-\u{1EE32}
- \u{1EE34}-\u{1EE37}
- \u{1EE39}
- \u{1EE3B}
- \u{1EE42}
- \u{1EE47}
- \u{1EE49}
- \u{1EE4B}
- \u{1EE4D}-\u{1EE4F}
- \u{1EE51}-\u{1EE52}
- \u{1EE54}
- \u{1EE57}
- \u{1EE59}
- \u{1EE5B}
- \u{1EE5D}
- \u{1EE5F}
- \u{1EE61}-\u{1EE62}
- \u{1EE64}
- \u{1EE67}-\u{1EE6A}
- \u{1EE6C}-\u{1EE72}
- \u{1EE74}-\u{1EE77}
- \u{1EE79}-\u{1EE7C}
- \u{1EE7E}
- \u{1EE80}-\u{1EE89}
- \u{1EE8B}-\u{1EE9B}
- \u{1EEA1}-\u{1EEA3}
- \u{1EEA5}-\u{1EEA9}
- \u{1EEAB}-\u{1EEBB}
- \u{1EEF0}-\u{1EEF1}
- \u{1F000}-\u{1F003}
- \u{1F005}-\u{1F02B}
- \u{1F030}-\u{1F093}
- \u{1F0A0}-\u{1F0AE}
- \u{1F0B1}-\u{1F0BF}
- \u{1F0C1}-\u{1F0CE}
- \u{1F0D1}-\u{1F0F5}
- \u{1F10B}-\u{1F10F}
- \u{1F12E}-\u{1F12F}
- \u{1F16A}-\u{1F16F}
- \u{1F1AD}
- \u{1F1E6}-\u{1F1FF}
- \u{1F321}-\u{1F32C}
- \u{1F336}
- \u{1F37D}
- \u{1F394}-\u{1F39F}
- \u{1F3CB}-\u{1F3CE}
- \u{1F3D4}-\u{1F3DF}
- \u{1F3F1}-\u{1F3F3}
- \u{1F3F5}-\u{1F3F7}
- \u{1F43F}
- \u{1F441}
- \u{1F4FD}-\u{1F4FE}
- \u{1F53E}-\u{1F54A}
- \u{1F54F}
- \u{1F568}-\u{1F579}
- \u{1F57B}-\u{1F594}
- \u{1F597}-\u{1F5A3}
- \u{1F5A5}-\u{1F5FA}
- \u{1F650}-\u{1F67F}
- \u{1F6C6}-\u{1F6CB}
- \u{1F6CD}-\u{1F6CF}
- \u{1F6D3}-\u{1F6D4}
- \u{1F6E0}-\u{1F6EA}
- \u{1F6F0}-\u{1F6F3}
- \u{1F700}-\u{1F776}
- \u{1F77B}-\u{1F7D9}
- \u{1F800}-\u{1F80B}
- \u{1F810}-\u{1F847}
- \u{1F850}-\u{1F859}
- \u{1F860}-\u{1F887}
- \u{1F890}-\u{1F8AD}
- \u{1F8B0}-\u{1F8B1}
- \u{1F900}-\u{1F90B}
- \u{1F93B}
- \u{1F946}
- \u{1FA00}-\u{1FA53}
- \u{1FA60}-\u{1FA6D}
- \u{1FB00}-\u{1FB92}
- \u{1FB94}-\u{1FBCA}
- \u{1FBF0}-\u{1FBF9}
- \u{E0001}
- \u{E0020}-\u{E007F}
- ).join }]/
+ CHUNK_LAST, CHUNK_WIDTH = [
+ [0x1f, 2],
+ [0x7e, 1],
+ [0x7f, 2],
+ [0xa0, 1],
+ [0xa1, -1],
+ [0xa3, 1],
+ [0xa4, -1],
+ [0xa6, 1],
+ [0xa8, -1],
+ [0xa9, 1],
+ [0xaa, -1],
+ [0xac, 1],
+ [0xae, -1],
+ [0xaf, 1],
+ [0xb4, -1],
+ [0xb5, 1],
+ [0xba, -1],
+ [0xbb, 1],
+ [0xbf, -1],
+ [0xc5, 1],
+ [0xc6, -1],
+ [0xcf, 1],
+ [0xd0, -1],
+ [0xd6, 1],
+ [0xd8, -1],
+ [0xdd, 1],
+ [0xe1, -1],
+ [0xe5, 1],
+ [0xe6, -1],
+ [0xe7, 1],
+ [0xea, -1],
+ [0xeb, 1],
+ [0xed, -1],
+ [0xef, 1],
+ [0xf0, -1],
+ [0xf1, 1],
+ [0xf3, -1],
+ [0xf6, 1],
+ [0xfa, -1],
+ [0xfb, 1],
+ [0xfc, -1],
+ [0xfd, 1],
+ [0xfe, -1],
+ [0x100, 1],
+ [0x101, -1],
+ [0x110, 1],
+ [0x111, -1],
+ [0x112, 1],
+ [0x113, -1],
+ [0x11a, 1],
+ [0x11b, -1],
+ [0x125, 1],
+ [0x127, -1],
+ [0x12a, 1],
+ [0x12b, -1],
+ [0x130, 1],
+ [0x133, -1],
+ [0x137, 1],
+ [0x138, -1],
+ [0x13e, 1],
+ [0x142, -1],
+ [0x143, 1],
+ [0x144, -1],
+ [0x147, 1],
+ [0x14b, -1],
+ [0x14c, 1],
+ [0x14d, -1],
+ [0x151, 1],
+ [0x153, -1],
+ [0x165, 1],
+ [0x167, -1],
+ [0x16a, 1],
+ [0x16b, -1],
+ [0x1cd, 1],
+ [0x1ce, -1],
+ [0x1cf, 1],
+ [0x1d0, -1],
+ [0x1d1, 1],
+ [0x1d2, -1],
+ [0x1d3, 1],
+ [0x1d4, -1],
+ [0x1d5, 1],
+ [0x1d6, -1],
+ [0x1d7, 1],
+ [0x1d8, -1],
+ [0x1d9, 1],
+ [0x1da, -1],
+ [0x1db, 1],
+ [0x1dc, -1],
+ [0x250, 1],
+ [0x251, -1],
+ [0x260, 1],
+ [0x261, -1],
+ [0x2c3, 1],
+ [0x2c4, -1],
+ [0x2c6, 1],
+ [0x2c7, -1],
+ [0x2c8, 1],
+ [0x2cb, -1],
+ [0x2cc, 1],
+ [0x2cd, -1],
+ [0x2cf, 1],
+ [0x2d0, -1],
+ [0x2d7, 1],
+ [0x2db, -1],
+ [0x2dc, 1],
+ [0x2dd, -1],
+ [0x2de, 1],
+ [0x2df, -1],
+ [0x2ff, 1],
+ [0x36f, 0],
+ [0x390, 1],
+ [0x3a1, -1],
+ [0x3a2, 1],
+ [0x3a9, -1],
+ [0x3b0, 1],
+ [0x3c1, -1],
+ [0x3c2, 1],
+ [0x3c9, -1],
+ [0x400, 1],
+ [0x401, -1],
+ [0x40f, 1],
+ [0x44f, -1],
+ [0x450, 1],
+ [0x451, -1],
+ [0x482, 1],
+ [0x487, 0],
+ [0x590, 1],
+ [0x5bd, 0],
+ [0x5be, 1],
+ [0x5bf, 0],
+ [0x5c0, 1],
+ [0x5c2, 0],
+ [0x5c3, 1],
+ [0x5c5, 0],
+ [0x5c6, 1],
+ [0x5c7, 0],
+ [0x60f, 1],
+ [0x61a, 0],
+ [0x64a, 1],
+ [0x65f, 0],
+ [0x66f, 1],
+ [0x670, 0],
+ [0x6d5, 1],
+ [0x6dc, 0],
+ [0x6de, 1],
+ [0x6e4, 0],
+ [0x6e6, 1],
+ [0x6e8, 0],
+ [0x6e9, 1],
+ [0x6ed, 0],
+ [0x710, 1],
+ [0x711, 0],
+ [0x72f, 1],
+ [0x74a, 0],
+ [0x7a5, 1],
+ [0x7b0, 0],
+ [0x7ea, 1],
+ [0x7f3, 0],
+ [0x7fc, 1],
+ [0x7fd, 0],
+ [0x815, 1],
+ [0x819, 0],
+ [0x81a, 1],
+ [0x823, 0],
+ [0x824, 1],
+ [0x827, 0],
+ [0x828, 1],
+ [0x82d, 0],
+ [0x858, 1],
+ [0x85b, 0],
+ [0x897, 1],
+ [0x89f, 0],
+ [0x8c9, 1],
+ [0x8e1, 0],
+ [0x8e2, 1],
+ [0x902, 0],
+ [0x939, 1],
+ [0x93a, 0],
+ [0x93b, 1],
+ [0x93c, 0],
+ [0x940, 1],
+ [0x948, 0],
+ [0x94c, 1],
+ [0x94d, 0],
+ [0x950, 1],
+ [0x957, 0],
+ [0x961, 1],
+ [0x963, 0],
+ [0x980, 1],
+ [0x981, 0],
+ [0x9bb, 1],
+ [0x9bc, 0],
+ [0x9c0, 1],
+ [0x9c4, 0],
+ [0x9cc, 1],
+ [0x9cd, 0],
+ [0x9e1, 1],
+ [0x9e3, 0],
+ [0x9fd, 1],
+ [0x9fe, 0],
+ [0xa00, 1],
+ [0xa02, 0],
+ [0xa3b, 1],
+ [0xa3c, 0],
+ [0xa40, 1],
+ [0xa42, 0],
+ [0xa46, 1],
+ [0xa48, 0],
+ [0xa4a, 1],
+ [0xa4d, 0],
+ [0xa50, 1],
+ [0xa51, 0],
+ [0xa6f, 1],
+ [0xa71, 0],
+ [0xa74, 1],
+ [0xa75, 0],
+ [0xa80, 1],
+ [0xa82, 0],
+ [0xabb, 1],
+ [0xabc, 0],
+ [0xac0, 1],
+ [0xac5, 0],
+ [0xac6, 1],
+ [0xac8, 0],
+ [0xacc, 1],
+ [0xacd, 0],
+ [0xae1, 1],
+ [0xae3, 0],
+ [0xaf9, 1],
+ [0xaff, 0],
+ [0xb00, 1],
+ [0xb01, 0],
+ [0xb3b, 1],
+ [0xb3c, 0],
+ [0xb3e, 1],
+ [0xb3f, 0],
+ [0xb40, 1],
+ [0xb44, 0],
+ [0xb4c, 1],
+ [0xb4d, 0],
+ [0xb54, 1],
+ [0xb56, 0],
+ [0xb61, 1],
+ [0xb63, 0],
+ [0xb81, 1],
+ [0xb82, 0],
+ [0xbbf, 1],
+ [0xbc0, 0],
+ [0xbcc, 1],
+ [0xbcd, 0],
+ [0xbff, 1],
+ [0xc00, 0],
+ [0xc03, 1],
+ [0xc04, 0],
+ [0xc3b, 1],
+ [0xc3c, 0],
+ [0xc3d, 1],
+ [0xc40, 0],
+ [0xc45, 1],
+ [0xc48, 0],
+ [0xc49, 1],
+ [0xc4d, 0],
+ [0xc54, 1],
+ [0xc56, 0],
+ [0xc61, 1],
+ [0xc63, 0],
+ [0xc80, 1],
+ [0xc81, 0],
+ [0xcbb, 1],
+ [0xcbc, 0],
+ [0xcbe, 1],
+ [0xcbf, 0],
+ [0xcc5, 1],
+ [0xcc6, 0],
+ [0xccb, 1],
+ [0xccd, 0],
+ [0xce1, 1],
+ [0xce3, 0],
+ [0xcff, 1],
+ [0xd01, 0],
+ [0xd3a, 1],
+ [0xd3c, 0],
+ [0xd40, 1],
+ [0xd44, 0],
+ [0xd4c, 1],
+ [0xd4d, 0],
+ [0xd61, 1],
+ [0xd63, 0],
+ [0xd80, 1],
+ [0xd81, 0],
+ [0xdc9, 1],
+ [0xdca, 0],
+ [0xdd1, 1],
+ [0xdd4, 0],
+ [0xdd5, 1],
+ [0xdd6, 0],
+ [0xe30, 1],
+ [0xe31, 0],
+ [0xe33, 1],
+ [0xe3a, 0],
+ [0xe46, 1],
+ [0xe4e, 0],
+ [0xeb0, 1],
+ [0xeb1, 0],
+ [0xeb3, 1],
+ [0xebc, 0],
+ [0xec7, 1],
+ [0xece, 0],
+ [0xf17, 1],
+ [0xf19, 0],
+ [0xf34, 1],
+ [0xf35, 0],
+ [0xf36, 1],
+ [0xf37, 0],
+ [0xf38, 1],
+ [0xf39, 0],
+ [0xf70, 1],
+ [0xf7e, 0],
+ [0xf7f, 1],
+ [0xf84, 0],
+ [0xf85, 1],
+ [0xf87, 0],
+ [0xf8c, 1],
+ [0xf97, 0],
+ [0xf98, 1],
+ [0xfbc, 0],
+ [0xfc5, 1],
+ [0xfc6, 0],
+ [0x102c, 1],
+ [0x1030, 0],
+ [0x1031, 1],
+ [0x1037, 0],
+ [0x1038, 1],
+ [0x103a, 0],
+ [0x103c, 1],
+ [0x103e, 0],
+ [0x1057, 1],
+ [0x1059, 0],
+ [0x105d, 1],
+ [0x1060, 0],
+ [0x1070, 1],
+ [0x1074, 0],
+ [0x1081, 1],
+ [0x1082, 0],
+ [0x1084, 1],
+ [0x1086, 0],
+ [0x108c, 1],
+ [0x108d, 0],
+ [0x109c, 1],
+ [0x109d, 0],
+ [0x10ff, 1],
+ [0x115f, 2],
+ [0x135c, 1],
+ [0x135f, 0],
+ [0x1711, 1],
+ [0x1714, 0],
+ [0x1731, 1],
+ [0x1733, 0],
+ [0x1751, 1],
+ [0x1753, 0],
+ [0x1771, 1],
+ [0x1773, 0],
+ [0x17b3, 1],
+ [0x17b5, 0],
+ [0x17b6, 1],
+ [0x17bd, 0],
+ [0x17c5, 1],
+ [0x17c6, 0],
+ [0x17c8, 1],
+ [0x17d3, 0],
+ [0x17dc, 1],
+ [0x17dd, 0],
+ [0x180a, 1],
+ [0x180d, 0],
+ [0x180e, 1],
+ [0x180f, 0],
+ [0x1884, 1],
+ [0x1886, 0],
+ [0x18a8, 1],
+ [0x18a9, 0],
+ [0x191f, 1],
+ [0x1922, 0],
+ [0x1926, 1],
+ [0x1928, 0],
+ [0x1931, 1],
+ [0x1932, 0],
+ [0x1938, 1],
+ [0x193b, 0],
+ [0x1a16, 1],
+ [0x1a18, 0],
+ [0x1a1a, 1],
+ [0x1a1b, 0],
+ [0x1a55, 1],
+ [0x1a56, 0],
+ [0x1a57, 1],
+ [0x1a5e, 0],
+ [0x1a5f, 1],
+ [0x1a60, 0],
+ [0x1a61, 1],
+ [0x1a62, 0],
+ [0x1a64, 1],
+ [0x1a6c, 0],
+ [0x1a72, 1],
+ [0x1a7c, 0],
+ [0x1a7e, 1],
+ [0x1a7f, 0],
+ [0x1aaf, 1],
+ [0x1abd, 0],
+ [0x1abe, 1],
+ [0x1ace, 0],
+ [0x1aff, 1],
+ [0x1b03, 0],
+ [0x1b33, 1],
+ [0x1b34, 0],
+ [0x1b35, 1],
+ [0x1b3a, 0],
+ [0x1b3b, 1],
+ [0x1b3c, 0],
+ [0x1b41, 1],
+ [0x1b42, 0],
+ [0x1b6a, 1],
+ [0x1b73, 0],
+ [0x1b7f, 1],
+ [0x1b81, 0],
+ [0x1ba1, 1],
+ [0x1ba5, 0],
+ [0x1ba7, 1],
+ [0x1ba9, 0],
+ [0x1baa, 1],
+ [0x1bad, 0],
+ [0x1be5, 1],
+ [0x1be6, 0],
+ [0x1be7, 1],
+ [0x1be9, 0],
+ [0x1bec, 1],
+ [0x1bed, 0],
+ [0x1bee, 1],
+ [0x1bf1, 0],
+ [0x1c2b, 1],
+ [0x1c33, 0],
+ [0x1c35, 1],
+ [0x1c37, 0],
+ [0x1ccf, 1],
+ [0x1cd2, 0],
+ [0x1cd3, 1],
+ [0x1ce0, 0],
+ [0x1ce1, 1],
+ [0x1ce8, 0],
+ [0x1cec, 1],
+ [0x1ced, 0],
+ [0x1cf3, 1],
+ [0x1cf4, 0],
+ [0x1cf7, 1],
+ [0x1cf9, 0],
+ [0x1dbf, 1],
+ [0x1dff, 0],
+ [0x200f, 1],
+ [0x2010, -1],
+ [0x2012, 1],
+ [0x2016, -1],
+ [0x2017, 1],
+ [0x2019, -1],
+ [0x201b, 1],
+ [0x201d, -1],
+ [0x201f, 1],
+ [0x2022, -1],
+ [0x2023, 1],
+ [0x2027, -1],
+ [0x202f, 1],
+ [0x2030, -1],
+ [0x2031, 1],
+ [0x2033, -1],
+ [0x2034, 1],
+ [0x2035, -1],
+ [0x203a, 1],
+ [0x203b, -1],
+ [0x203d, 1],
+ [0x203e, -1],
+ [0x2073, 1],
+ [0x2074, -1],
+ [0x207e, 1],
+ [0x207f, -1],
+ [0x2080, 1],
+ [0x2084, -1],
+ [0x20ab, 1],
+ [0x20ac, -1],
+ [0x20cf, 1],
+ [0x20dc, 0],
+ [0x20e0, 1],
+ [0x20e1, 0],
+ [0x20e4, 1],
+ [0x20f0, 0],
+ [0x2102, 1],
+ [0x2103, -1],
+ [0x2104, 1],
+ [0x2105, -1],
+ [0x2108, 1],
+ [0x2109, -1],
+ [0x2112, 1],
+ [0x2113, -1],
+ [0x2115, 1],
+ [0x2116, -1],
+ [0x2120, 1],
+ [0x2122, -1],
+ [0x2125, 1],
+ [0x2126, -1],
+ [0x212a, 1],
+ [0x212b, -1],
+ [0x2152, 1],
+ [0x2154, -1],
+ [0x215a, 1],
+ [0x215e, -1],
+ [0x215f, 1],
+ [0x216b, -1],
+ [0x216f, 1],
+ [0x2179, -1],
+ [0x2188, 1],
+ [0x2189, -1],
+ [0x218f, 1],
+ [0x2199, -1],
+ [0x21b7, 1],
+ [0x21b9, -1],
+ [0x21d1, 1],
+ [0x21d2, -1],
+ [0x21d3, 1],
+ [0x21d4, -1],
+ [0x21e6, 1],
+ [0x21e7, -1],
+ [0x21ff, 1],
+ [0x2200, -1],
+ [0x2201, 1],
+ [0x2203, -1],
+ [0x2206, 1],
+ [0x2208, -1],
+ [0x220a, 1],
+ [0x220b, -1],
+ [0x220e, 1],
+ [0x220f, -1],
+ [0x2210, 1],
+ [0x2211, -1],
+ [0x2214, 1],
+ [0x2215, -1],
+ [0x2219, 1],
+ [0x221a, -1],
+ [0x221c, 1],
+ [0x2220, -1],
+ [0x2222, 1],
+ [0x2223, -1],
+ [0x2224, 1],
+ [0x2225, -1],
+ [0x2226, 1],
+ [0x222c, -1],
+ [0x222d, 1],
+ [0x222e, -1],
+ [0x2233, 1],
+ [0x2237, -1],
+ [0x223b, 1],
+ [0x223d, -1],
+ [0x2247, 1],
+ [0x2248, -1],
+ [0x224b, 1],
+ [0x224c, -1],
+ [0x2251, 1],
+ [0x2252, -1],
+ [0x225f, 1],
+ [0x2261, -1],
+ [0x2263, 1],
+ [0x2267, -1],
+ [0x2269, 1],
+ [0x226b, -1],
+ [0x226d, 1],
+ [0x226f, -1],
+ [0x2281, 1],
+ [0x2283, -1],
+ [0x2285, 1],
+ [0x2287, -1],
+ [0x2294, 1],
+ [0x2295, -1],
+ [0x2298, 1],
+ [0x2299, -1],
+ [0x22a4, 1],
+ [0x22a5, -1],
+ [0x22be, 1],
+ [0x22bf, -1],
+ [0x2311, 1],
+ [0x2312, -1],
+ [0x2319, 1],
+ [0x231b, 2],
+ [0x2328, 1],
+ [0x232a, 2],
+ [0x23e8, 1],
+ [0x23ec, 2],
+ [0x23ef, 1],
+ [0x23f0, 2],
+ [0x23f2, 1],
+ [0x23f3, 2],
+ [0x245f, 1],
+ [0x24e9, -1],
+ [0x24ea, 1],
+ [0x254b, -1],
+ [0x254f, 1],
+ [0x2573, -1],
+ [0x257f, 1],
+ [0x258f, -1],
+ [0x2591, 1],
+ [0x2595, -1],
+ [0x259f, 1],
+ [0x25a1, -1],
+ [0x25a2, 1],
+ [0x25a9, -1],
+ [0x25b1, 1],
+ [0x25b3, -1],
+ [0x25b5, 1],
+ [0x25b7, -1],
+ [0x25bb, 1],
+ [0x25bd, -1],
+ [0x25bf, 1],
+ [0x25c1, -1],
+ [0x25c5, 1],
+ [0x25c8, -1],
+ [0x25ca, 1],
+ [0x25cb, -1],
+ [0x25cd, 1],
+ [0x25d1, -1],
+ [0x25e1, 1],
+ [0x25e5, -1],
+ [0x25ee, 1],
+ [0x25ef, -1],
+ [0x25fc, 1],
+ [0x25fe, 2],
+ [0x2604, 1],
+ [0x2606, -1],
+ [0x2608, 1],
+ [0x2609, -1],
+ [0x260d, 1],
+ [0x260f, -1],
+ [0x2613, 1],
+ [0x2615, 2],
+ [0x261b, 1],
+ [0x261c, -1],
+ [0x261d, 1],
+ [0x261e, -1],
+ [0x263f, 1],
+ [0x2640, -1],
+ [0x2641, 1],
+ [0x2642, -1],
+ [0x2647, 1],
+ [0x2653, 2],
+ [0x265f, 1],
+ [0x2661, -1],
+ [0x2662, 1],
+ [0x2665, -1],
+ [0x2666, 1],
+ [0x266a, -1],
+ [0x266b, 1],
+ [0x266d, -1],
+ [0x266e, 1],
+ [0x266f, -1],
+ [0x267e, 1],
+ [0x267f, 2],
+ [0x2692, 1],
+ [0x2693, 2],
+ [0x269d, 1],
+ [0x269f, -1],
+ [0x26a0, 1],
+ [0x26a1, 2],
+ [0x26a9, 1],
+ [0x26ab, 2],
+ [0x26bc, 1],
+ [0x26be, 2],
+ [0x26bf, -1],
+ [0x26c3, 1],
+ [0x26c5, 2],
+ [0x26cd, -1],
+ [0x26ce, 2],
+ [0x26d3, -1],
+ [0x26d4, 2],
+ [0x26e1, -1],
+ [0x26e2, 1],
+ [0x26e3, -1],
+ [0x26e7, 1],
+ [0x26e9, -1],
+ [0x26ea, 2],
+ [0x26f1, -1],
+ [0x26f3, 2],
+ [0x26f4, -1],
+ [0x26f5, 2],
+ [0x26f9, -1],
+ [0x26fa, 2],
+ [0x26fc, -1],
+ [0x26fd, 2],
+ [0x26ff, -1],
+ [0x2704, 1],
+ [0x2705, 2],
+ [0x2709, 1],
+ [0x270b, 2],
+ [0x2727, 1],
+ [0x2728, 2],
+ [0x273c, 1],
+ [0x273d, -1],
+ [0x274b, 1],
+ [0x274c, 2],
+ [0x274d, 1],
+ [0x274e, 2],
+ [0x2752, 1],
+ [0x2755, 2],
+ [0x2756, 1],
+ [0x2757, 2],
+ [0x2775, 1],
+ [0x277f, -1],
+ [0x2794, 1],
+ [0x2797, 2],
+ [0x27af, 1],
+ [0x27b0, 2],
+ [0x27be, 1],
+ [0x27bf, 2],
+ [0x2b1a, 1],
+ [0x2b1c, 2],
+ [0x2b4f, 1],
+ [0x2b50, 2],
+ [0x2b54, 1],
+ [0x2b55, 2],
+ [0x2b59, -1],
+ [0x2cee, 1],
+ [0x2cf1, 0],
+ [0x2d7e, 1],
+ [0x2d7f, 0],
+ [0x2ddf, 1],
+ [0x2dff, 0],
+ [0x2e7f, 1],
+ [0x2e99, 2],
+ [0x2e9a, 1],
+ [0x2ef3, 2],
+ [0x2eff, 1],
+ [0x2fd5, 2],
+ [0x2fef, 1],
+ [0x3029, 2],
+ [0x302d, 0],
+ [0x303e, 2],
+ [0x3040, 1],
+ [0x3096, 2],
+ [0x3098, 1],
+ [0x309a, 0],
+ [0x30ff, 2],
+ [0x3104, 1],
+ [0x312f, 2],
+ [0x3130, 1],
+ [0x318e, 2],
+ [0x318f, 1],
+ [0x31e3, 2],
+ [0x31ee, 1],
+ [0x321e, 2],
+ [0x321f, 1],
+ [0x3247, 2],
+ [0x324f, -1],
+ [0x4dbf, 2],
+ [0x4dff, 1],
+ [0xa48c, 2],
+ [0xa48f, 1],
+ [0xa4c6, 2],
+ [0xa66e, 1],
+ [0xa66f, 0],
+ [0xa673, 1],
+ [0xa67d, 0],
+ [0xa69d, 1],
+ [0xa69f, 0],
+ [0xa6ef, 1],
+ [0xa6f1, 0],
+ [0xa801, 1],
+ [0xa802, 0],
+ [0xa805, 1],
+ [0xa806, 0],
+ [0xa80a, 1],
+ [0xa80b, 0],
+ [0xa824, 1],
+ [0xa826, 0],
+ [0xa82b, 1],
+ [0xa82c, 0],
+ [0xa8c3, 1],
+ [0xa8c5, 0],
+ [0xa8df, 1],
+ [0xa8f1, 0],
+ [0xa8fe, 1],
+ [0xa8ff, 0],
+ [0xa925, 1],
+ [0xa92d, 0],
+ [0xa946, 1],
+ [0xa951, 0],
+ [0xa95f, 1],
+ [0xa97c, 2],
+ [0xa97f, 1],
+ [0xa982, 0],
+ [0xa9b2, 1],
+ [0xa9b3, 0],
+ [0xa9b5, 1],
+ [0xa9b9, 0],
+ [0xa9bb, 1],
+ [0xa9bd, 0],
+ [0xa9e4, 1],
+ [0xa9e5, 0],
+ [0xaa28, 1],
+ [0xaa2e, 0],
+ [0xaa30, 1],
+ [0xaa32, 0],
+ [0xaa34, 1],
+ [0xaa36, 0],
+ [0xaa42, 1],
+ [0xaa43, 0],
+ [0xaa4b, 1],
+ [0xaa4c, 0],
+ [0xaa7b, 1],
+ [0xaa7c, 0],
+ [0xaaaf, 1],
+ [0xaab0, 0],
+ [0xaab1, 1],
+ [0xaab4, 0],
+ [0xaab6, 1],
+ [0xaab8, 0],
+ [0xaabd, 1],
+ [0xaabf, 0],
+ [0xaac0, 1],
+ [0xaac1, 0],
+ [0xaaeb, 1],
+ [0xaaed, 0],
+ [0xaaf5, 1],
+ [0xaaf6, 0],
+ [0xabe4, 1],
+ [0xabe5, 0],
+ [0xabe7, 1],
+ [0xabe8, 0],
+ [0xabec, 1],
+ [0xabed, 0],
+ [0xabff, 1],
+ [0xd7a3, 2],
+ [0xdfff, 1],
+ [0xf8ff, -1],
+ [0xfaff, 2],
+ [0xfb1d, 1],
+ [0xfb1e, 0],
+ [0xfdff, 1],
+ [0xfe0f, 0],
+ [0xfe19, 2],
+ [0xfe1f, 1],
+ [0xfe2f, 0],
+ [0xfe52, 2],
+ [0xfe53, 1],
+ [0xfe66, 2],
+ [0xfe67, 1],
+ [0xfe6b, 2],
+ [0xff00, 1],
+ [0xff60, 2],
+ [0xffdf, 1],
+ [0xffe6, 2],
+ [0xfffc, 1],
+ [0xfffd, -1],
+ [0x101fc, 1],
+ [0x101fd, 0],
+ [0x102df, 1],
+ [0x102e0, 0],
+ [0x10375, 1],
+ [0x1037a, 0],
+ [0x10a00, 1],
+ [0x10a03, 0],
+ [0x10a04, 1],
+ [0x10a06, 0],
+ [0x10a0b, 1],
+ [0x10a0f, 0],
+ [0x10a37, 1],
+ [0x10a3a, 0],
+ [0x10a3e, 1],
+ [0x10a3f, 0],
+ [0x10ae4, 1],
+ [0x10ae6, 0],
+ [0x10d23, 1],
+ [0x10d27, 0],
+ [0x10eaa, 1],
+ [0x10eac, 0],
+ [0x10efc, 1],
+ [0x10eff, 0],
+ [0x10f45, 1],
+ [0x10f50, 0],
+ [0x10f81, 1],
+ [0x10f85, 0],
+ [0x11000, 1],
+ [0x11001, 0],
+ [0x11037, 1],
+ [0x11046, 0],
+ [0x1106f, 1],
+ [0x11070, 0],
+ [0x11072, 1],
+ [0x11074, 0],
+ [0x1107e, 1],
+ [0x11081, 0],
+ [0x110b2, 1],
+ [0x110b6, 0],
+ [0x110b8, 1],
+ [0x110ba, 0],
+ [0x110c1, 1],
+ [0x110c2, 0],
+ [0x110ff, 1],
+ [0x11102, 0],
+ [0x11126, 1],
+ [0x1112b, 0],
+ [0x1112c, 1],
+ [0x11134, 0],
+ [0x11172, 1],
+ [0x11173, 0],
+ [0x1117f, 1],
+ [0x11181, 0],
+ [0x111b5, 1],
+ [0x111be, 0],
+ [0x111c8, 1],
+ [0x111cc, 0],
+ [0x111ce, 1],
+ [0x111cf, 0],
+ [0x1122e, 1],
+ [0x11231, 0],
+ [0x11233, 1],
+ [0x11234, 0],
+ [0x11235, 1],
+ [0x11237, 0],
+ [0x1123d, 1],
+ [0x1123e, 0],
+ [0x11240, 1],
+ [0x11241, 0],
+ [0x112de, 1],
+ [0x112df, 0],
+ [0x112e2, 1],
+ [0x112ea, 0],
+ [0x112ff, 1],
+ [0x11301, 0],
+ [0x1133a, 1],
+ [0x1133c, 0],
+ [0x1133f, 1],
+ [0x11340, 0],
+ [0x11365, 1],
+ [0x1136c, 0],
+ [0x1136f, 1],
+ [0x11374, 0],
+ [0x11437, 1],
+ [0x1143f, 0],
+ [0x11441, 1],
+ [0x11444, 0],
+ [0x11445, 1],
+ [0x11446, 0],
+ [0x1145d, 1],
+ [0x1145e, 0],
+ [0x114b2, 1],
+ [0x114b8, 0],
+ [0x114b9, 1],
+ [0x114ba, 0],
+ [0x114be, 1],
+ [0x114c0, 0],
+ [0x114c1, 1],
+ [0x114c3, 0],
+ [0x115b1, 1],
+ [0x115b5, 0],
+ [0x115bb, 1],
+ [0x115bd, 0],
+ [0x115be, 1],
+ [0x115c0, 0],
+ [0x115db, 1],
+ [0x115dd, 0],
+ [0x11632, 1],
+ [0x1163a, 0],
+ [0x1163c, 1],
+ [0x1163d, 0],
+ [0x1163e, 1],
+ [0x11640, 0],
+ [0x116aa, 1],
+ [0x116ab, 0],
+ [0x116ac, 1],
+ [0x116ad, 0],
+ [0x116af, 1],
+ [0x116b5, 0],
+ [0x116b6, 1],
+ [0x116b7, 0],
+ [0x1171c, 1],
+ [0x1171f, 0],
+ [0x11721, 1],
+ [0x11725, 0],
+ [0x11726, 1],
+ [0x1172b, 0],
+ [0x1182e, 1],
+ [0x11837, 0],
+ [0x11838, 1],
+ [0x1183a, 0],
+ [0x1193a, 1],
+ [0x1193c, 0],
+ [0x1193d, 1],
+ [0x1193e, 0],
+ [0x11942, 1],
+ [0x11943, 0],
+ [0x119d3, 1],
+ [0x119d7, 0],
+ [0x119d9, 1],
+ [0x119db, 0],
+ [0x119df, 1],
+ [0x119e0, 0],
+ [0x11a00, 1],
+ [0x11a0a, 0],
+ [0x11a32, 1],
+ [0x11a38, 0],
+ [0x11a3a, 1],
+ [0x11a3e, 0],
+ [0x11a46, 1],
+ [0x11a47, 0],
+ [0x11a50, 1],
+ [0x11a56, 0],
+ [0x11a58, 1],
+ [0x11a5b, 0],
+ [0x11a89, 1],
+ [0x11a96, 0],
+ [0x11a97, 1],
+ [0x11a99, 0],
+ [0x11c2f, 1],
+ [0x11c36, 0],
+ [0x11c37, 1],
+ [0x11c3d, 0],
+ [0x11c3e, 1],
+ [0x11c3f, 0],
+ [0x11c91, 1],
+ [0x11ca7, 0],
+ [0x11ca9, 1],
+ [0x11cb0, 0],
+ [0x11cb1, 1],
+ [0x11cb3, 0],
+ [0x11cb4, 1],
+ [0x11cb6, 0],
+ [0x11d30, 1],
+ [0x11d36, 0],
+ [0x11d39, 1],
+ [0x11d3a, 0],
+ [0x11d3b, 1],
+ [0x11d3d, 0],
+ [0x11d3e, 1],
+ [0x11d45, 0],
+ [0x11d46, 1],
+ [0x11d47, 0],
+ [0x11d8f, 1],
+ [0x11d91, 0],
+ [0x11d94, 1],
+ [0x11d95, 0],
+ [0x11d96, 1],
+ [0x11d97, 0],
+ [0x11ef2, 1],
+ [0x11ef4, 0],
+ [0x11eff, 1],
+ [0x11f01, 0],
+ [0x11f35, 1],
+ [0x11f3a, 0],
+ [0x11f3f, 1],
+ [0x11f40, 0],
+ [0x11f41, 1],
+ [0x11f42, 0],
+ [0x1343f, 1],
+ [0x13440, 0],
+ [0x13446, 1],
+ [0x13455, 0],
+ [0x16aef, 1],
+ [0x16af4, 0],
+ [0x16b2f, 1],
+ [0x16b36, 0],
+ [0x16f4e, 1],
+ [0x16f4f, 0],
+ [0x16f8e, 1],
+ [0x16f92, 0],
+ [0x16fdf, 1],
+ [0x16fe3, 2],
+ [0x16fe4, 0],
+ [0x16fef, 1],
+ [0x16ff1, 2],
+ [0x16fff, 1],
+ [0x187f7, 2],
+ [0x187ff, 1],
+ [0x18cd5, 2],
+ [0x18cff, 1],
+ [0x18d08, 2],
+ [0x1afef, 1],
+ [0x1aff3, 2],
+ [0x1aff4, 1],
+ [0x1affb, 2],
+ [0x1affc, 1],
+ [0x1affe, 2],
+ [0x1afff, 1],
+ [0x1b122, 2],
+ [0x1b131, 1],
+ [0x1b132, 2],
+ [0x1b14f, 1],
+ [0x1b152, 2],
+ [0x1b154, 1],
+ [0x1b155, 2],
+ [0x1b163, 1],
+ [0x1b167, 2],
+ [0x1b16f, 1],
+ [0x1b2fb, 2],
+ [0x1bc9c, 1],
+ [0x1bc9e, 0],
+ [0x1ceff, 1],
+ [0x1cf2d, 0],
+ [0x1cf2f, 1],
+ [0x1cf46, 0],
+ [0x1d166, 1],
+ [0x1d169, 0],
+ [0x1d17a, 1],
+ [0x1d182, 0],
+ [0x1d184, 1],
+ [0x1d18b, 0],
+ [0x1d1a9, 1],
+ [0x1d1ad, 0],
+ [0x1d241, 1],
+ [0x1d244, 0],
+ [0x1d9ff, 1],
+ [0x1da36, 0],
+ [0x1da3a, 1],
+ [0x1da6c, 0],
+ [0x1da74, 1],
+ [0x1da75, 0],
+ [0x1da83, 1],
+ [0x1da84, 0],
+ [0x1da9a, 1],
+ [0x1da9f, 0],
+ [0x1daa0, 1],
+ [0x1daaf, 0],
+ [0x1dfff, 1],
+ [0x1e006, 0],
+ [0x1e007, 1],
+ [0x1e018, 0],
+ [0x1e01a, 1],
+ [0x1e021, 0],
+ [0x1e022, 1],
+ [0x1e024, 0],
+ [0x1e025, 1],
+ [0x1e02a, 0],
+ [0x1e08e, 1],
+ [0x1e08f, 0],
+ [0x1e12f, 1],
+ [0x1e136, 0],
+ [0x1e2ad, 1],
+ [0x1e2ae, 0],
+ [0x1e2eb, 1],
+ [0x1e2ef, 0],
+ [0x1e4eb, 1],
+ [0x1e4ef, 0],
+ [0x1e8cf, 1],
+ [0x1e8d6, 0],
+ [0x1e943, 1],
+ [0x1e94a, 0],
+ [0x1f003, 1],
+ [0x1f004, 2],
+ [0x1f0ce, 1],
+ [0x1f0cf, 2],
+ [0x1f0ff, 1],
+ [0x1f10a, -1],
+ [0x1f10f, 1],
+ [0x1f12d, -1],
+ [0x1f12f, 1],
+ [0x1f169, -1],
+ [0x1f16f, 1],
+ [0x1f18d, -1],
+ [0x1f18e, 2],
+ [0x1f190, -1],
+ [0x1f19a, 2],
+ [0x1f1ac, -1],
+ [0x1f1ff, 1],
+ [0x1f202, 2],
+ [0x1f20f, 1],
+ [0x1f23b, 2],
+ [0x1f23f, 1],
+ [0x1f248, 2],
+ [0x1f24f, 1],
+ [0x1f251, 2],
+ [0x1f25f, 1],
+ [0x1f265, 2],
+ [0x1f2ff, 1],
+ [0x1f320, 2],
+ [0x1f32c, 1],
+ [0x1f335, 2],
+ [0x1f336, 1],
+ [0x1f37c, 2],
+ [0x1f37d, 1],
+ [0x1f393, 2],
+ [0x1f39f, 1],
+ [0x1f3ca, 2],
+ [0x1f3ce, 1],
+ [0x1f3d3, 2],
+ [0x1f3df, 1],
+ [0x1f3f0, 2],
+ [0x1f3f3, 1],
+ [0x1f3f4, 2],
+ [0x1f3f7, 1],
+ [0x1f43e, 2],
+ [0x1f43f, 1],
+ [0x1f440, 2],
+ [0x1f441, 1],
+ [0x1f4fc, 2],
+ [0x1f4fe, 1],
+ [0x1f53d, 2],
+ [0x1f54a, 1],
+ [0x1f54e, 2],
+ [0x1f54f, 1],
+ [0x1f567, 2],
+ [0x1f579, 1],
+ [0x1f57a, 2],
+ [0x1f594, 1],
+ [0x1f596, 2],
+ [0x1f5a3, 1],
+ [0x1f5a4, 2],
+ [0x1f5fa, 1],
+ [0x1f64f, 2],
+ [0x1f67f, 1],
+ [0x1f6c5, 2],
+ [0x1f6cb, 1],
+ [0x1f6cc, 2],
+ [0x1f6cf, 1],
+ [0x1f6d2, 2],
+ [0x1f6d4, 1],
+ [0x1f6d7, 2],
+ [0x1f6db, 1],
+ [0x1f6df, 2],
+ [0x1f6ea, 1],
+ [0x1f6ec, 2],
+ [0x1f6f3, 1],
+ [0x1f6fc, 2],
+ [0x1f7df, 1],
+ [0x1f7eb, 2],
+ [0x1f7ef, 1],
+ [0x1f7f0, 2],
+ [0x1f90b, 1],
+ [0x1f93a, 2],
+ [0x1f93b, 1],
+ [0x1f945, 2],
+ [0x1f946, 1],
+ [0x1f9ff, 2],
+ [0x1fa6f, 1],
+ [0x1fa7c, 2],
+ [0x1fa7f, 1],
+ [0x1fa88, 2],
+ [0x1fa8f, 1],
+ [0x1fabd, 2],
+ [0x1fabe, 1],
+ [0x1fac5, 2],
+ [0x1facd, 1],
+ [0x1fadb, 2],
+ [0x1fadf, 1],
+ [0x1fae8, 2],
+ [0x1faef, 1],
+ [0x1faf8, 2],
+ [0x1ffff, 1],
+ [0x2fffd, 2],
+ [0x2ffff, 1],
+ [0x3fffd, 2],
+ [0xe00ff, 1],
+ [0xe01ef, 0],
+ [0xeffff, 1],
+ [0xffffd, -1],
+ [0xfffff, 1],
+ [0x10fffd, -1],
+ [0x7fffffff, 1]
+ ].transpose.map(&:freeze)
end
diff --git a/lib/reline/version.rb b/lib/reline/version.rb
index ef7d617a45..b75d874adb 100644
--- a/lib/reline/version.rb
+++ b/lib/reline/version.rb
@@ -1,3 +1,3 @@
module Reline
- VERSION = '0.5.6'
+ VERSION = '0.5.10'
end
diff --git a/lib/resolv.gemspec b/lib/resolv.gemspec
index 6b83e303d7..bfa2f9ff31 100644
--- a/lib/resolv.gemspec
+++ b/lib/resolv.gemspec
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
spec.homepage = "https://github.com/ruby/resolv"
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
spec.licenses = ["Ruby", "BSD-2-Clause"]
+ spec.extensions << "ext/win32/resolv/extconf.rb"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
diff --git a/lib/resolv.rb b/lib/resolv.rb
index e36dbce259..0574ce622b 100644
--- a/lib/resolv.rb
+++ b/lib/resolv.rb
@@ -513,35 +513,40 @@ class Resolv
def fetch_resource(name, typeclass)
lazy_initialize
- begin
- requester = make_udp_requester
+ truncated = {}
+ requesters = {}
+ udp_requester = begin
+ make_udp_requester
rescue Errno::EACCES
# fall back to TCP
end
senders = {}
+
begin
- @config.resolv(name) {|candidate, tout, nameserver, port|
- requester ||= make_tcp_requester(nameserver, port)
+ @config.resolv(name) do |candidate, tout, nameserver, port|
msg = Message.new
msg.rd = 1
msg.add_question(candidate, typeclass)
- unless sender = senders[[candidate, nameserver, port]]
+
+ requester = requesters.fetch([nameserver, port]) do
+ if !truncated[candidate] && udp_requester
+ udp_requester
+ else
+ requesters[[nameserver, port]] = make_tcp_requester(nameserver, port)
+ end
+ end
+
+ unless sender = senders[[candidate, requester, nameserver, port]]
sender = requester.sender(msg, candidate, nameserver, port)
next if !sender
- senders[[candidate, nameserver, port]] = sender
+ senders[[candidate, requester, nameserver, port]] = sender
end
reply, reply_name = requester.request(sender, tout)
case reply.rcode
when RCode::NoError
if reply.tc == 1 and not Requester::TCP === requester
- requester.close
# Retry via TCP:
- requester = make_tcp_requester(nameserver, port)
- senders = {}
- # This will use TCP for all remaining candidates (assuming the
- # current candidate does not already respond successfully via
- # TCP). This makes sense because we already know the full
- # response will not fit in an untruncated UDP packet.
+ truncated[candidate] = true
redo
else
yield(reply, reply_name)
@@ -552,9 +557,10 @@ class Resolv
else
raise Config::OtherResolvError.new(reply_name.to_s)
end
- }
+ end
ensure
- requester&.close
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
end
end
@@ -569,6 +575,11 @@ class Resolv
def make_tcp_requester(host, port) # :nodoc:
return Requester::TCP.new(host, port)
+ rescue Errno::ECONNREFUSED
+ # Treat a refused TCP connection attempt to a nameserver like a timeout,
+ # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
+ # hint to try the next nameserver:
+ raise ResolvTimeout
end
def extract_resources(msg, name, typeclass) # :nodoc:
@@ -1800,7 +1811,6 @@ class Resolv
end
end
-
##
# Base class for SvcParam. [RFC9460]
@@ -2499,7 +2509,6 @@ class Resolv
attr_reader :altitude
-
def encode_rdata(msg) # :nodoc:
msg.put_bytes(@version)
msg.put_bytes(@ssize.scalar)
@@ -3439,4 +3448,3 @@ class Resolv
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
end
-
diff --git a/lib/ruby_vm/rjit/assembler.rb b/lib/ruby_vm/rjit/assembler.rb
index 645072d11b..42995e6c8c 100644
--- a/lib/ruby_vm/rjit/assembler.rb
+++ b/lib/ruby_vm/rjit/assembler.rb
@@ -152,6 +152,16 @@ module RubyVM::RJIT
mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg],
disp: imm8(src_disp),
)
+ # AND r64, r/m64 (Mod 10: [reg]+disp32)
+ in [R64 => dst_reg, QwordPtr[R64 => src_reg, IMM32 => src_disp]]
+ # REX.W + 23 /r
+ # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r)
+ insn(
+ prefix: REX_W,
+ opcode: 0x23,
+ mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg],
+ disp: imm32(src_disp),
+ )
end
end
@@ -736,6 +746,16 @@ module RubyVM::RJIT
mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg],
disp: imm8(src_disp),
)
+ # OR r64, r/m64 (Mod 10: [reg]+disp32)
+ in [R64 => dst_reg, QwordPtr[R64 => src_reg, IMM32 => src_disp]]
+ # REX.W + 0B /r
+ # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r)
+ insn(
+ prefix: REX_W,
+ opcode: 0x0b,
+ mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg],
+ disp: imm32(src_disp),
+ )
end
end
diff --git a/lib/ruby_vm/rjit/insn_compiler.rb b/lib/ruby_vm/rjit/insn_compiler.rb
index f9450241c9..a33ba9f468 100644
--- a/lib/ruby_vm/rjit/insn_compiler.rb
+++ b/lib/ruby_vm/rjit/insn_compiler.rb
@@ -63,7 +63,6 @@ module RubyVM::RJIT
when :toregexp then toregexp(jit, ctx, asm)
when :intern then intern(jit, ctx, asm)
when :newarray then newarray(jit, ctx, asm)
- # newarraykwsplat
when :duparray then duparray(jit, ctx, asm)
# duphash
when :expandarray then expandarray(jit, ctx, asm)
@@ -91,6 +90,8 @@ module RubyVM::RJIT
when :opt_send_without_block then opt_send_without_block(jit, ctx, asm)
when :objtostring then objtostring(jit, ctx, asm)
when :opt_str_freeze then opt_str_freeze(jit, ctx, asm)
+ when :opt_ary_freeze then opt_ary_freeze(jit, ctx, asm)
+ when :opt_hash_freeze then opt_hash_freeze(jit, ctx, asm)
when :opt_nil_p then opt_nil_p(jit, ctx, asm)
# opt_str_uminus
when :opt_newarray_send then opt_newarray_send(jit, ctx, asm)
@@ -504,7 +505,7 @@ module RubyVM::RJIT
shape = C.rb_shape_get_shape_by_id(shape_id)
current_capacity = shape.capacity
- dest_shape = C.rb_shape_get_next(shape, comptime_receiver, ivar_name)
+ dest_shape = C.rb_shape_get_next_no_warnings(shape, comptime_receiver, ivar_name)
new_shape_id = C.rb_shape_id(dest_shape)
if new_shape_id == C::OBJ_TOO_COMPLEX_SHAPE_ID
@@ -944,8 +945,6 @@ module RubyVM::RJIT
KeepCompiling
end
- # newarraykwsplat
-
# @param jit [RubyVM::RJIT::JITState]
# @param ctx [RubyVM::RJIT::Context]
# @param asm [RubyVM::RJIT::Assembler]
@@ -1435,6 +1434,10 @@ module RubyVM::RJIT
mid = C.vm_ci_mid(cd.ci)
calling = build_calling(ci: cd.ci, block_handler: blockiseq)
+ if calling.flags & C::VM_CALL_FORWARDING != 0
+ return CantCompile
+ end
+
# vm_sendish
cme, comptime_recv_klass = jit_search_method(jit, ctx, asm, mid, calling)
if cme == CantCompile
@@ -1492,6 +1495,42 @@ module RubyVM::RJIT
# @param jit [RubyVM::RJIT::JITState]
# @param ctx [RubyVM::RJIT::Context]
# @param asm [RubyVM::RJIT::Assembler]
+ def opt_ary_freeze(jit, ctx, asm)
+ unless Invariants.assume_bop_not_redefined(jit, C::ARRAY_REDEFINED_OP_FLAG, C::BOP_FREEZE)
+ return CantCompile;
+ end
+
+ ary = jit.operand(0, ruby: true)
+
+ # Push the return value onto the stack
+ stack_ret = ctx.stack_push(Type::CArray)
+ asm.mov(:rax, to_value(ary))
+ asm.mov(stack_ret, :rax)
+
+ KeepCompiling
+ end
+
+ # @param jit [RubyVM::RJIT::JITState]
+ # @param ctx [RubyVM::RJIT::Context]
+ # @param asm [RubyVM::RJIT::Assembler]
+ def opt_hash_freeze(jit, ctx, asm)
+ unless Invariants.assume_bop_not_redefined(jit, C::HASH_REDEFINED_OP_FLAG, C::BOP_FREEZE)
+ return CantCompile;
+ end
+
+ hash = jit.operand(0, ruby: true)
+
+ # Push the return value onto the stack
+ stack_ret = ctx.stack_push(Type::CHash)
+ asm.mov(:rax, to_value(hash))
+ asm.mov(stack_ret, :rax)
+
+ KeepCompiling
+ end
+
+ # @param jit [RubyVM::RJIT::JITState]
+ # @param ctx [RubyVM::RJIT::Context]
+ # @param asm [RubyVM::RJIT::Assembler]
def opt_str_freeze(jit, ctx, asm)
unless Invariants.assume_bop_not_redefined(jit, C::STRING_REDEFINED_OP_FLAG, C::BOP_FREEZE)
return CantCompile;
@@ -4622,6 +4661,11 @@ module RubyVM::RJIT
end
end
+ # Don't compile forwardable iseqs
+ if iseq.body.param.flags.forwardable
+ return CantCompile
+ end
+
# We will not have CantCompile from here.
if block_arg
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
index ad7ab10756..43c0d8f13c 100644
--- a/lib/rubygems.rb
+++ b/lib/rubygems.rb
@@ -18,6 +18,7 @@ require_relative "rubygems/compatibility"
require_relative "rubygems/defaults"
require_relative "rubygems/deprecate"
require_relative "rubygems/errors"
+require_relative "rubygems/target_rbconfig"
##
# RubyGems is the Ruby standard for publishing and managing third party
@@ -179,6 +180,8 @@ module Gem
@discover_gems_on_require = true
+ @target_rbconfig = nil
+
##
# Try to activate a gem containing +path+. Returns true if
# activation succeeded or wasn't needed because it was already
@@ -397,6 +400,23 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
end
##
+ # The RbConfig object for the deployment target platform.
+ #
+ # This is usually the same as the running platform, but may be
+ # different if you are cross-compiling.
+
+ def self.target_rbconfig
+ @target_rbconfig || Gem::TargetRbConfig.for_running_ruby
+ end
+
+ def self.set_target_rbconfig(rbconfig_path)
+ @target_rbconfig = Gem::TargetRbConfig.from_path(rbconfig_path)
+ Gem::Platform.local(refresh: true)
+ Gem.platforms << Gem::Platform.local unless Gem.platforms.include? Gem::Platform.local
+ @target_rbconfig
+ end
+
+ ##
# Quietly ensure the Gem directory +dir+ contains all the proper
# subdirectories. If we can't create a directory due to a permission
# problem, then we will silently continue.
@@ -450,7 +470,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
# distinction as extensions cannot be shared between the two.
def self.extension_api_version # :nodoc:
- if RbConfig::CONFIG["ENABLE_SHARED"] == "no"
+ if target_rbconfig["ENABLE_SHARED"] == "no"
"#{ruby_api_version}-static"
else
ruby_api_version
@@ -753,17 +773,14 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
# Safely read a file in binary mode on all platforms.
def self.read_binary(path)
- open_file(path, "rb+", &:read)
- rescue Errno::EACCES, Errno::EROFS
- open_file(path, "rb", &:read)
+ File.binread(path)
end
##
# Safely write a file in binary mode on all platforms.
+
def self.write_binary(path, data)
- open_file(path, "wb") do |io|
- io.write data
- end
+ File.binwrite(path, data)
rescue Errno::ENOSPC
# If we ran out of space but the file exists, it's *guaranteed* to be corrupted.
File.delete(path) if File.exist?(path)
@@ -771,26 +788,38 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
end
##
- # Open a file with given flags, and on Windows protect access with flock
+ # Open a file with given flags
def self.open_file(path, flags, &block)
- File.open(path, flags) do |io|
- if !java_platform? && win_platform?
- begin
- io.flock(File::LOCK_EX)
- rescue Errno::ENOSYS, Errno::ENOTSUP
- end
+ File.open(path, flags, &block)
+ end
+
+ ##
+ # Open a file with given flags, and protect access with a file lock
+
+ def self.open_file_with_lock(path, &block)
+ file_lock = "#{path}.lock"
+ open_file_with_flock(file_lock, &block)
+ ensure
+ FileUtils.rm_f file_lock
+ end
+
+ ##
+ # Open a file with given flags, and protect access with flock
+
+ def self.open_file_with_flock(path, &block)
+ mode = IO::RDONLY | IO::APPEND | IO::CREAT | IO::BINARY
+ mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE)
+
+ File.open(path, mode) do |io|
+ begin
+ io.flock(File::LOCK_EX)
+ rescue Errno::ENOSYS, Errno::ENOTSUP
+ rescue Errno::ENOLCK # NFS
+ raise unless Thread.main == Thread.current
end
yield io
end
- rescue Errno::ENOLCK # NFS
- if Thread.main != Thread.current
- raise
- else
- File.open(path, flags) do |io|
- yield io
- end
- end
end
##
@@ -810,7 +839,7 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
# Returns a String containing the API compatibility version of Ruby
def self.ruby_api_version
- @ruby_api_version ||= RbConfig::CONFIG["ruby_version"].dup
+ @ruby_api_version ||= target_rbconfig["ruby_version"].dup
end
def self.env_requirement(gem_name)
@@ -1013,6 +1042,13 @@ An Array (#{env.inspect}) was passed in from #{caller[3]}
end
##
+ # Is this platform FreeBSD
+
+ def self.freebsd_platform?
+ RbConfig::CONFIG["host_os"].to_s.include?("bsd")
+ end
+
+ ##
# Load +plugins+ as Ruby files
def self.load_plugin_files(plugins) # :nodoc:
diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb
index 0380fceece..b76716acf5 100644
--- a/lib/rubygems/basic_specification.rb
+++ b/lib/rubygems/basic_specification.rb
@@ -99,6 +99,20 @@ class Gem::BasicSpecification
end
##
+ # Regular gems take precedence over default gems
+
+ def default_gem_priority
+ default_gem? ? 1 : -1
+ end
+
+ ##
+ # Gems higher up in +gem_path+ take precedence
+
+ def base_dir_priority(gem_path)
+ gem_path.index(base_dir) || gem_path.size
+ end
+
+ ##
# Returns full path to the directory where gem's extensions are installed.
def extension_dir
@@ -125,11 +139,11 @@ class Gem::BasicSpecification
# The full path to the gem (install path + full name).
def full_gem_path
- # TODO: This is a heavily used method by gems, so we'll need
- # to aleast just alias it to #gem_dir rather than remove it.
@full_gem_path ||= find_full_gem_path
end
+ alias_method :gem_dir, :full_gem_path
+
##
# Returns the full name (name-version) of this Gem. Platform information
# is included (name-version-platform) if it is specified and not the
@@ -144,6 +158,19 @@ class Gem::BasicSpecification
end
##
+ # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`).
+ # Information about where the gem is installed is also included if not
+ # installed in the default GEM_HOME.
+
+ def full_name_with_location
+ if base_dir != Gem.dir
+ "#{full_name} in #{base_dir}"
+ else
+ full_name
+ end
+ end
+
+ ##
# Full paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
# activated.
@@ -187,14 +214,6 @@ class Gem::BasicSpecification
end
##
- # Returns the full path to this spec's gem directory.
- # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0
-
- def gem_dir
- @gem_dir ||= File.expand_path File.join(gems_dir, full_name)
- end
-
- ##
# Returns the full path to the gems directory containing this spec's
# gem directory. eg: /usr/local/lib/ruby/1.8/gems
diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb
index dd2fd77418..4ebbad1c0c 100644
--- a/lib/rubygems/bundler_version_finder.rb
+++ b/lib/rubygems/bundler_version_finder.rb
@@ -21,7 +21,7 @@ module Gem::BundlerVersionFinder
end
def self.bundle_update_bundler_version
- return unless File.basename($0) == "bundle"
+ return unless ["bundle", "bundler"].include? File.basename($0)
return unless "update".start_with?(ARGV.first || " ")
bundler_version = nil
update_index = nil
diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb
index d588804290..7985b0fda6 100644
--- a/lib/rubygems/commands/exec_command.rb
+++ b/lib/rubygems/commands/exec_command.rb
@@ -57,8 +57,6 @@ to the same gem path as user-installed gems.
end
def execute
- gem_paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => Gem.paths.path.join(File::PATH_SEPARATOR), "GEM_SPEC_CACHE" => Gem.paths.spec_cache_dir }.compact
-
check_executable
print_command
@@ -74,9 +72,6 @@ to the same gem path as user-installed gems.
end
load!
- ensure
- ENV.update(gem_paths) if gem_paths
- Gem.clear_paths
end
private
@@ -143,7 +138,7 @@ to the same gem path as user-installed gems.
end
def set_gem_exec_install_paths
- home = File.join(Gem.dir, "gem_exec")
+ home = Gem.dir
ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR)
ENV["GEM_HOME"] = home
diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb
index f7f5b62306..c524cf7922 100644
--- a/lib/rubygems/commands/fetch_command.rb
+++ b/lib/rubygems/commands/fetch_command.rb
@@ -63,6 +63,17 @@ then repackaging it.
def execute
check_version
+
+ exit_code = fetch_gems
+
+ terminate_interaction exit_code
+ end
+
+ private
+
+ def fetch_gems
+ exit_code = 0
+
version = options[:version]
platform = Gem.platforms.last
@@ -86,10 +97,13 @@ then repackaging it.
if spec.nil?
show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain]
+ exit_code |= 2
next
end
source.download spec
say "Downloaded #{spec.full_name}"
end
+
+ exit_code
end
end
diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb
index 2091634a29..2888b6c55a 100644
--- a/lib/rubygems/commands/install_command.rb
+++ b/lib/rubygems/commands/install_command.rb
@@ -224,10 +224,6 @@ You can use `i` command instead of `install`.
rescue Gem::InstallError => e
alert_error "Error installing #{gem_name}:\n\t#{e.message}"
exit_code |= 1
- rescue Gem::GemNotFoundException => e
- show_lookup_failure e.name, e.version, e.errors, suppress_suggestions
-
- exit_code |= 2
rescue Gem::UnsatisfiableDependencyError => e
show_lookup_failure e.name, e.version, e.errors, suppress_suggestions,
"'#{gem_name}' (#{gem_version})"
diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb
index 456d897df2..999c9fef0f 100644
--- a/lib/rubygems/commands/pristine_command.rb
+++ b/lib/rubygems/commands/pristine_command.rb
@@ -57,7 +57,7 @@ class Gem::Commands::PristineCommand < Gem::Command
end
add_option("-i", "--install-dir DIR",
- "Gem repository to get binstubs and plugins installed") do |value, options|
+ "Gem repository to get gems restored") do |value, options|
options[:install_dir] = File.expand_path(value)
end
@@ -103,21 +103,25 @@ extensions will be restored.
end
def execute
+ install_dir = options[:install_dir]
+
+ specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record
+
specs = if options[:all]
- Gem::Specification.map
+ specification_record.map
# `--extensions` must be explicitly given to pristine only gems
# with extensions.
elsif options[:extensions_set] &&
options[:extensions] && options[:args].empty?
- Gem::Specification.select do |spec|
+ specification_record.select do |spec|
spec.extensions && !spec.extensions.empty?
end
elsif options[:only_missing_extensions]
- Gem::Specification.select(&:missing_extensions?)
+ specification_record.select(&:missing_extensions?)
else
get_all_gem_names.sort.map do |gem_name|
- Gem::Specification.find_all_by_name(gem_name, options[:version]).reverse
+ specification_record.find_all_by_name(gem_name, options[:version]).reverse
end.flatten
end
@@ -144,7 +148,7 @@ extensions will be restored.
end
unless spec.extensions.empty? || options[:extensions] || options[:only_executables] || options[:only_plugins]
- say "Skipped #{spec.full_name}, it needs to compile an extension"
+ say "Skipped #{spec.full_name_with_location}, it needs to compile an extension"
next
end
@@ -153,7 +157,7 @@ extensions will be restored.
unless File.exist?(gem) || options[:only_executables] || options[:only_plugins]
require_relative "../remote_fetcher"
- say "Cached gem for #{spec.full_name} not found, attempting to fetch..."
+ say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..."
dep = Gem::Dependency.new spec.name, spec.version
found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
@@ -176,7 +180,6 @@ extensions will be restored.
end
bin_dir = options[:bin_dir] if options[:bin_dir]
- install_dir = options[:install_dir] if options[:install_dir]
installer_options = {
wrappers: true,
@@ -198,7 +201,7 @@ extensions will be restored.
installer.install
end
- say "Restored #{spec.full_name}"
+ say "Restored #{spec.full_name_with_location}"
end
end
end
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb
index 3f38074280..bb2246ca31 100644
--- a/lib/rubygems/commands/setup_command.rb
+++ b/lib/rubygems/commands/setup_command.rb
@@ -279,11 +279,7 @@ By default, this RubyGems will install gem as:
File.open bin_cmd_file, "w" do |file|
file.puts <<-TEXT
@ECHO OFF
- IF NOT "%~f0" == "~f0" GOTO :WinNT
- @"#{File.basename(Gem.ruby).chomp('"')}" "#{dest_file}" %1 %2 %3 %4 %5 %6 %7 %8 %9
- GOTO :EOF
- :WinNT
- @"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
+ @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
TEXT
end
@@ -340,6 +336,8 @@ By default, this RubyGems will install gem as:
require_relative "../rdoc"
+ return false unless defined?(Gem::RDoc)
+
fake_spec = Gem::Specification.new "rubygems", Gem::VERSION
def fake_spec.full_gem_path
File.expand_path "../../..", __dir__
@@ -585,6 +583,8 @@ abort "#{deprecation_message}"
args = %w[--all --only-executables --silent]
args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
if options[:env_shebang]
args << "--env-shebang"
end
diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb
index 2a77ec72cf..3d6e41e49e 100644
--- a/lib/rubygems/commands/uninstall_command.rb
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -157,9 +157,14 @@ that is a dependency of an existing gem. You can use the
gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name])
- say("Gem '#{name}' is not installed") if gem_specs.empty?
- gem_specs.each do |spec|
- deplist.add spec
+ if gem_specs.empty?
+ say("Gem '#{name}' is not installed")
+ else
+ gem_specs.reject!(&:default_gem?) if gem_specs.size > 1
+
+ gem_specs.each do |spec|
+ deplist.add spec
+ end
end
end
@@ -184,7 +189,7 @@ that is a dependency of an existing gem. You can use the
rescue Gem::GemNotInHomeException => e
spec = e.spec
alert("In order to remove #{spec.name}, please execute:\n" \
- "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}")
+ "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}")
rescue Gem::UninstallError => e
spec = e.spec
alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \
diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb
index 7874ad0dc9..a2bcb6dfbc 100644
--- a/lib/rubygems/config_file.rb
+++ b/lib/rubygems/config_file.rb
@@ -522,12 +522,12 @@ if you believe they were disclosed to a third party.
# Return the configuration information for +key+.
def [](key)
- @hash[key.to_s]
+ @hash[key] || @hash[key.to_s]
end
# Set configuration option +key+ to +value+.
def []=(key, value)
- @hash[key.to_s] = value
+ @hash[key] = value
end
def ==(other) # :nodoc:
@@ -555,8 +555,13 @@ if you believe they were disclosed to a third party.
require_relative "yaml_serializer"
content = Gem::YAMLSerializer.load(yaml)
+ deep_transform_config_keys!(content)
+ end
- content.transform_keys! do |k|
+ private
+
+ def self.deep_transform_config_keys!(config)
+ config.transform_keys! do |k|
if k.match?(/\A:(.*)\Z/)
k[1..-1].to_sym
elsif k.include?("__") || k.match?(%r{/\Z})
@@ -570,7 +575,7 @@ if you believe they were disclosed to a third party.
end
end
- content.transform_values! do |v|
+ config.transform_values! do |v|
if v.is_a?(String)
if v.match?(/\A:(.*)\Z/)
v[1..-1].to_sym
@@ -583,18 +588,18 @@ if you believe they were disclosed to a third party.
else
v
end
- elsif v.is_a?(Hash) && v.empty?
+ elsif v.empty?
nil
+ elsif v.is_a?(Hash)
+ deep_transform_config_keys!(v)
else
v
end
end
- content
+ config
end
- private
-
def set_config_file_name(args)
@config_file_name = ENV["GEMRC"]
need_config_file_name = false
diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb
index d1bf074441..ecb4824d7e 100644
--- a/lib/rubygems/dependency.rb
+++ b/lib/rubygems/dependency.rb
@@ -271,15 +271,7 @@ class Gem::Dependency
end
def matching_specs(platform_only = false)
- env_req = Gem.env_requirement(name)
- matches = Gem::Specification.stubs_for(name).find_all do |spec|
- requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version)
- end.map(&:to_spec)
-
- if prioritizes_bundler?
- require_relative "bundler_version_finder"
- Gem::BundlerVersionFinder.prioritize!(matches)
- end
+ matches = Gem::Specification.find_all_by_name(name, requirement)
if platform_only
matches.reject! do |spec|
@@ -297,10 +289,6 @@ class Gem::Dependency
@requirement.specific?
end
- def prioritizes_bundler?
- name == "bundler" && !specific?
- end
-
def to_specs
matches = matching_specs true
@@ -349,4 +337,12 @@ class Gem::Dependency
:released
end
end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "requirement", @requirement
+ coder.add "type", @type
+ coder.add "prerelease", @prerelease
+ coder.add "version_requirements", @version_requirements
+ end
end
diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb
index 0308b4687f..793324b875 100644
--- a/lib/rubygems/exceptions.rb
+++ b/lib/rubygems/exceptions.rb
@@ -104,9 +104,6 @@ end
class Gem::GemNotFoundException < Gem::Exception; end
-##
-# Raised by the DependencyInstaller when a specific gem cannot be found
-
class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException
##
# Creates a new SpecificGemNotFoundException for a gem with the given +name+
@@ -137,6 +134,8 @@ class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException
attr_reader :errors
end
+Gem.deprecate_constant :SpecificGemNotFoundException
+
##
# Raised by Gem::Resolver when dependencies conflict and create the
# inability to find a valid possible spec for a request.
diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb
index be1ba3031c..12eb62ef16 100644
--- a/lib/rubygems/ext/builder.rb
+++ b/lib/rubygems/ext/builder.rb
@@ -19,13 +19,14 @@ class Gem::Ext::Builder
$1.downcase
end
- def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"])
+ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"],
+ target_rbconfig: Gem.target_rbconfig)
unless File.exist? File.join(make_dir, "Makefile")
raise Gem::InstallError, "Makefile not found"
end
# try to find make program from Ruby configure arguments first
- RbConfig::CONFIG["configure_args"] =~ /with-make-prog\=(\w+)/
+ target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/
make_program_name = ENV["MAKE"] || ENV["make"] || $1
make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make"
make_program = Shellwords.split(make_program_name)
@@ -131,10 +132,11 @@ class Gem::Ext::Builder
# have build arguments, saved, set +build_args+ which is an ARGV-style
# array.
- def initialize(spec, build_args = spec.build_args)
+ def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig)
@spec = spec
@build_args = build_args
@gem_dir = spec.full_gem_path
+ @target_rbconfig = target_rbconfig
@ran_rake = false
end
@@ -191,7 +193,7 @@ EOF
FileUtils.mkdir_p dest_path
results = builder.build(extension, dest_path,
- results, @build_args, lib_dir, extension_dir)
+ results, @build_args, lib_dir, extension_dir, @target_rbconfig)
verbose { results.join("\n") }
diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb
index 09ad1407c2..81b28c3c77 100644
--- a/lib/rubygems/ext/cargo_builder.rb
+++ b/lib/rubygems/ext/cargo_builder.rb
@@ -16,10 +16,15 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder
@profile = :release
end
- def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd)
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd,
+ target_rbconfig=Gem.target_rbconfig)
require "tempfile"
require "fileutils"
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring"
+ end
+
# Where's the Cargo.toml of the crate we're building
cargo_toml = File.join(cargo_dir, "Cargo.toml")
# What's the crate's name
diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb
index b162664784..34564f668d 100644
--- a/lib/rubygems/ext/cmake_builder.rb
+++ b/lib/rubygems/ext/cmake_builder.rb
@@ -1,7 +1,12 @@
# frozen_string_literal: true
class Gem::Ext::CmakeBuilder < Gem::Ext::Builder
- def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd)
+ def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd,
+ target_rbconfig=Gem.target_rbconfig)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring"
+ end
+
unless File.exist?(File.join(cmake_dir, "Makefile"))
require_relative "../command"
cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args]
@@ -9,7 +14,7 @@ class Gem::Ext::CmakeBuilder < Gem::Ext::Builder
run cmd, results, class_name, cmake_dir
end
- make dest_path, results, cmake_dir
+ make dest_path, results, cmake_dir, target_rbconfig: target_rbconfig
results
end
diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb
index 6b8590ba2e..d91b1ec5e8 100644
--- a/lib/rubygems/ext/configure_builder.rb
+++ b/lib/rubygems/ext/configure_builder.rb
@@ -7,14 +7,19 @@
#++
class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder
- def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd)
+ def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd,
+ target_rbconfig=Gem.target_rbconfig)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring"
+ end
+
unless File.exist?(File.join(configure_dir, "Makefile"))
cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args]
run cmd, results, class_name, configure_dir
end
- make dest_path, results, configure_dir
+ make dest_path, results, configure_dir, target_rbconfig: target_rbconfig
results
end
diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb
index fb68a7a8cc..e652a221f8 100644
--- a/lib/rubygems/ext/ext_conf_builder.rb
+++ b/lib/rubygems/ext/ext_conf_builder.rb
@@ -7,7 +7,8 @@
#++
class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
- def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd)
+ def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd,
+ target_rbconfig=Gem.target_rbconfig)
require "fileutils"
require "tempfile"
@@ -23,6 +24,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
begin
cmd = ruby << File.basename(extension)
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path
cmd.push(*args)
run(cmd, results, class_name, extension_dir) do |s, r|
@@ -39,11 +41,14 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
ENV["DESTDIR"] = nil
- make dest_path, results, extension_dir, tmp_dest_relative
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig
full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
- if Gem.install_extension_in_lib && lib_dir
+ is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
+ # Do not copy extension libraries by default when cross-compiling
+ # not to conflict with the one already built for the host platform.
+ if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling
FileUtils.mkdir_p lib_dir
entries = Dir.entries(full_tmp_dest) - %w[. ..]
entries = entries.map {|entry| File.join full_tmp_dest, entry }
@@ -55,7 +60,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
destent.exist? || FileUtils.mv(ent.path, destent.path)
end
- make dest_path, results, extension_dir, tmp_dest_relative, ["clean"]
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
ensure
ENV["DESTDIR"] = destdir
end
diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb
index 0171807b39..42393a4a06 100644
--- a/lib/rubygems/ext/rake_builder.rb
+++ b/lib/rubygems/ext/rake_builder.rb
@@ -9,7 +9,12 @@ require_relative "../shellwords"
#++
class Gem::Ext::RakeBuilder < Gem::Ext::Builder
- def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd)
+ def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd,
+ target_rbconfig=Gem.target_rbconfig)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring"
+ end
+
if /mkrf_conf/i.match?(File.basename(extension))
run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
end
diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb
index aad207a718..0d0f0dc211 100644
--- a/lib/rubygems/install_update_options.rb
+++ b/lib/rubygems/install_update_options.rb
@@ -179,6 +179,11 @@ module Gem::InstallUpdateOptions
"Suggest alternates when gems are not found") do |v,_o|
options[:suggest_alternate] = v
end
+
+ add_option(:"Install/Update", "--target-rbconfig [FILE]",
+ "rbconfig.rb for the deployment target platform") do |v, _o|
+ Gem.set_target_rbconfig(v)
+ end
end
##
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
index 8f6f9a5aa8..735d30ab9e 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -344,7 +344,7 @@ class Gem::Installer
say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil?
- Gem::Specification.add_spec(spec)
+ Gem::Specification.add_spec(spec) unless @install_dir
load_plugin
@@ -500,8 +500,7 @@ class Gem::Installer
dir_mode = options[:prog_mode] || (mode | 0o111)
unless dir_mode == mode
- require "fileutils"
- FileUtils.chmod dir_mode, bin_path
+ File.chmod dir_mode, bin_path
end
check_executable_overwrite filename
@@ -539,12 +538,14 @@ class Gem::Installer
def generate_bin_script(filename, bindir)
bin_script_path = File.join bindir, formatted_program_filename(filename)
- require "fileutils"
- FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
+ FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
- File.open bin_script_path, "wb", 0o755 do |file|
- file.print app_script_text(filename)
- file.chmod(options[:prog_mode] || 0o755)
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
+ end
end
verbose bin_script_path
@@ -847,7 +848,7 @@ TEXT
# configure scripts and rakefiles or mkrf_conf files.
def build_extensions
- builder = Gem::Ext::Builder.new spec, build_args
+ builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig
builder.build_extensions
end
@@ -993,7 +994,7 @@ TEXT
end
def rb_config
- RbConfig::CONFIG
+ Gem.target_rbconfig
end
def ruby_install_name
diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb
index d54ad12880..450c214167 100644
--- a/lib/rubygems/platform.rb
+++ b/lib/rubygems/platform.rb
@@ -12,9 +12,10 @@ class Gem::Platform
attr_accessor :cpu, :os, :version
- def self.local
- @local ||= begin
- arch = RbConfig::CONFIG["arch"]
+ def self.local(refresh: false)
+ return @local if @local && !refresh
+ @local = begin
+ arch = Gem.target_rbconfig["arch"]
arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch)
new(arch)
end
@@ -176,7 +177,7 @@ class Gem::Platform
# they have the same version, or either one has no version
#
# Additionally, the platform will match if the local CPU is 'arm' and the
- # other CPU starts with "arm" (for generic ARM family support).
+ # other CPU starts with "armv" (for generic 32-bit ARM family support).
#
# Of note, this method is not commutative. Indeed the OS 'linux' has a
# special case: the version is the libc name, yet while "no version" stands
@@ -197,7 +198,7 @@ class Gem::Platform
# cpu
([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu ||
- (@cpu == "arm" && other.cpu.start_with?("arm"))) &&
+ (@cpu == "arm" && other.cpu.start_with?("armv"))) &&
# os
@os == other.os &&
diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb
index a95a759401..ea05969422 100644
--- a/lib/rubygems/query_utils.rb
+++ b/lib/rubygems/query_utils.rb
@@ -132,7 +132,7 @@ module Gem::QueryUtils
version_matches = show_prereleases? || !s.version.prerelease?
name_matches && version_matches
- end
+ end.uniq(&:full_name)
spec_tuples = specs.map do |spec|
[spec.name_tuple, spec]
diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb
index c3a41592f6..4b5c74e0ea 100644
--- a/lib/rubygems/remote_fetcher.rb
+++ b/lib/rubygems/remote_fetcher.rb
@@ -75,7 +75,6 @@ class Gem::RemoteFetcher
def initialize(proxy=nil, dns=nil, headers={})
require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled
require_relative "vendored_net_http"
- require "stringio"
require_relative "vendor/uri/lib/uri"
Socket.do_not_reverse_lookup = true
diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb
index 02543cb14a..d9796c4208 100644
--- a/lib/rubygems/requirement.rb
+++ b/lib/rubygems/requirement.rb
@@ -13,8 +13,8 @@ class Gem::Requirement
OPS = { # :nodoc:
"=" => lambda {|v, r| v == r },
"!=" => lambda {|v, r| v != r },
- ">" => lambda {|v, r| v > r },
- "<" => lambda {|v, r| v < r },
+ ">" => lambda {|v, r| v > r },
+ "<" => lambda {|v, r| v < r },
">=" => lambda {|v, r| v >= r },
"<=" => lambda {|v, r| v <= r },
"~>" => lambda {|v, r| v >= r && v.release < r.bump },
@@ -214,10 +214,6 @@ class Gem::Requirement
yaml_initialize coder.tag, coder.map
end
- def to_yaml_properties # :nodoc:
- ["@requirements"]
- end
-
def encode_with(coder) # :nodoc:
coder.add "requirements", @requirements
end
diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb
index fc9ff58f57..5c722001b1 100644
--- a/lib/rubygems/resolver/activation_request.rb
+++ b/lib/rubygems/resolver/activation_request.rb
@@ -106,7 +106,7 @@ class Gem::Resolver::ActivationRequest
this_spec = full_spec
Gem::Specification.any? do |s|
- s == this_spec
+ s == this_spec && s.base_dir == this_spec.base_dir
end
end
end
diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb
index a983f8c6b6..57d0d00375 100644
--- a/lib/rubygems/resolver/best_set.rb
+++ b/lib/rubygems/resolver/best_set.rb
@@ -29,10 +29,8 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
pick_sets if @remote && @sets.empty?
super
- rescue Gem::RemoteFetcher::FetchError => e
- replace_failed_api_set e
-
- retry
+ rescue Gem::RemoteFetcher::FetchError
+ []
end
def prefetch(reqs) # :nodoc:
@@ -50,28 +48,4 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
q.pp @sets
end
end
-
- ##
- # Replaces a failed APISet for the URI in +error+ with an IndexSet.
- #
- # If no matching APISet can be found the original +error+ is raised.
- #
- # The calling method must retry the exception to repeat the lookup.
-
- def replace_failed_api_set(error) # :nodoc:
- uri = error.original_uri
- uri = Gem::URI uri unless Gem::URI === uri
- uri += "."
-
- raise error unless api_set = @sets.find do |set|
- Gem::Resolver::APISet === set && set.dep_uri == uri
- end
-
- index_set = Gem::Resolver::IndexSet.new api_set.source
-
- @sets.map! do |set|
- next set unless set == api_set
- index_set
- end
- end
end
diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb
index d90e311b65..bee5681dab 100644
--- a/lib/rubygems/source.rb
+++ b/lib/rubygems/source.rb
@@ -79,7 +79,7 @@ class Gem::Source
uri
end
- bundler_api_uri = enforce_trailing_slash(fetch_uri)
+ bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions"
begin
fetcher = Gem::RemoteFetcher.fetcher
@@ -213,14 +213,16 @@ class Gem::Source
end
def pretty_print(q) # :nodoc:
- q.group 2, "[Remote:", "]" do
- q.breakable
- q.text @uri.to_s
-
- if api = uri
+ q.object_group(self) do
+ q.group 2, "[Remote:", "]" do
q.breakable
- q.text "API URI: "
- q.text api.to_s
+ q.text @uri.to_s
+
+ if api = uri
+ q.breakable
+ q.text "API URI: "
+ q.text api.to_s
+ end
end
end
end
diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb
index bda63c6844..34f6851bc4 100644
--- a/lib/rubygems/source/git.rb
+++ b/lib/rubygems/source/git.rb
@@ -157,12 +157,14 @@ class Gem::Source::Git < Gem::Source
end
def pretty_print(q) # :nodoc:
- q.group 2, "[Git: ", "]" do
- q.breakable
- q.text @repository
+ q.object_group(self) do
+ q.group 2, "[Git: ", "]" do
+ q.breakable
+ q.text @repository
- q.breakable
- q.text @reference
+ q.breakable
+ q.text @reference
+ end
end
end
diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb
index cbe12a0516..f5c96fee51 100644
--- a/lib/rubygems/source/installed.rb
+++ b/lib/rubygems/source/installed.rb
@@ -32,6 +32,8 @@ class Gem::Source::Installed < Gem::Source
end
def pretty_print(q) # :nodoc:
- q.text "[Installed]"
+ q.object_group(self) do
+ q.text "[Installed]"
+ end
end
end
diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb
index d81d8343a8..ba6eea1f9a 100644
--- a/lib/rubygems/source/local.rb
+++ b/lib/rubygems/source/local.rb
@@ -117,10 +117,14 @@ class Gem::Source::Local < Gem::Source
end
def pretty_print(q) # :nodoc:
- q.group 2, "[Local gems:", "]" do
- q.breakable
- q.seplist @specs.keys do |v|
- q.text v.full_name
+ q.object_group(self) do
+ q.group 2, "[Local gems:", "]" do
+ q.breakable
+ if @specs
+ q.seplist @specs.keys do |v|
+ q.text v.full_name
+ end
+ end
end
end
end
diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb
index e9b2753646..dde1d48a21 100644
--- a/lib/rubygems/source/specific_file.rb
+++ b/lib/rubygems/source/specific_file.rb
@@ -42,9 +42,11 @@ class Gem::Source::SpecificFile < Gem::Source
end
def pretty_print(q) # :nodoc:
- q.group 2, "[SpecificFile:", "]" do
- q.breakable
- q.text @path
+ q.object_group(self) do
+ q.group 2, "[SpecificFile:", "]" do
+ q.breakable
+ q.text @path
+ end
end
end
diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb
index a1eaf1248e..5735038ad3 100644
--- a/lib/rubygems/specification.rb
+++ b/lib/rubygems/specification.rb
@@ -11,6 +11,7 @@ require_relative "deprecate"
require_relative "basic_specification"
require_relative "stub_specification"
require_relative "platform"
+require_relative "specification_record"
require_relative "util/list"
require "rbconfig"
@@ -174,24 +175,14 @@ class Gem::Specification < Gem::BasicSpecification
end
@@attributes = @@default_value.keys.sort_by(&:to_s)
- @@array_attributes = @@default_value.reject {|_k,v| v != [] }.keys
+ @@array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
@@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k|
@@default_value[k].nil?
end
- def self.clear_specs # :nodoc:
- @@all = nil
- @@stubs = nil
- @@stubs_by_name = {}
- @@spec_with_requirable_file = {}
- @@active_stub_with_requirable_file = {}
- end
- private_class_method :clear_specs
-
- clear_specs
-
# Sentinel object to represent "not found" stubs
NOT_FOUND = Struct.new(:to_spec, :this).new # :nodoc:
+ deprecate_constant :NOT_FOUND
# Tracking removed method calls to warn users during build time.
REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc:
@@ -555,9 +546,9 @@ class Gem::Specification < Gem::BasicSpecification
#
# Usage:
#
- # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4'
+ # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4'
- def add_runtime_dependency(gem, *requirements)
+ def add_dependency(gem, *requirements)
if requirements.uniq.size != requirements.size
warn "WARNING: duplicated #{gem} dependency #{requirements}"
end
@@ -770,7 +761,7 @@ class Gem::Specification < Gem::BasicSpecification
attr_accessor :specification_version
def self._all # :nodoc:
- @@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec)
+ specification_record.all
end
def self.clear_load_cache # :nodoc:
@@ -780,6 +771,11 @@ class Gem::Specification < Gem::BasicSpecification
end
private_class_method :clear_load_cache
+ def self.gem_path # :nodoc:
+ Gem.path
+ end
+ private_class_method :gem_path
+
def self.each_gemspec(dirs) # :nodoc:
dirs.each do |dir|
Gem::Util.glob_files_in_dir("*.gemspec", dir).each do |path|
@@ -788,26 +784,9 @@ class Gem::Specification < Gem::BasicSpecification
end
end
- def self.gemspec_stubs_in(dir, pattern)
+ def self.gemspec_stubs_in(dir, pattern) # :nodoc:
Gem::Util.glob_files_in_dir(pattern, dir).map {|path| yield path }.select(&:valid?)
end
- private_class_method :gemspec_stubs_in
-
- def self.installed_stubs(dirs, pattern)
- map_stubs(dirs, pattern) do |path, base_dir, gems_dir|
- Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir)
- end
- end
- private_class_method :installed_stubs
-
- def self.map_stubs(dirs, pattern) # :nodoc:
- dirs.flat_map do |dir|
- base_dir = File.dirname dir
- gems_dir = File.join base_dir, "gems"
- gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir }
- end
- end
- private_class_method :map_stubs
def self.each_spec(dirs) # :nodoc:
each_gemspec(dirs) do |path|
@@ -820,13 +799,7 @@ class Gem::Specification < Gem::BasicSpecification
# Returns a Gem::StubSpecification for every installed gem
def self.stubs
- @@stubs ||= begin
- pattern = "*.gemspec"
- stubs = stubs_for_pattern(pattern, false)
-
- @@stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name)
- stubs
- end
+ specification_record.stubs
end
##
@@ -845,13 +818,7 @@ class Gem::Specification < Gem::BasicSpecification
# only returns stubs that match Gem.platforms
def self.stubs_for(name)
- if @@stubs
- @@stubs_by_name[name] || []
- else
- @@stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s|
- s.name == name
- end
- end
+ specification_record.stubs_for(name)
end
##
@@ -859,12 +826,7 @@ class Gem::Specification < Gem::BasicSpecification
# optionally filtering out specs not matching the current platform
#
def self.stubs_for_pattern(pattern, match_platform = true) # :nodoc:
- installed_stubs = installed_stubs(Gem::Specification.dirs, pattern)
- installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform
- stubs = installed_stubs + default_stubs(pattern)
- stubs = stubs.uniq(&:full_name)
- _resort!(stubs)
- stubs
+ specification_record.stubs_for_pattern(pattern, match_platform)
end
def self._resort!(specs) # :nodoc:
@@ -873,7 +835,11 @@ class Gem::Specification < Gem::BasicSpecification
next names if names.nonzero?
versions = b.version <=> a.version
next versions if versions.nonzero?
- Gem::Platform.sort_priority(b.platform)
+ platforms = Gem::Platform.sort_priority(b.platform) <=> Gem::Platform.sort_priority(a.platform)
+ next platforms if platforms.nonzero?
+ default_gem = a.default_gem_priority <=> b.default_gem_priority
+ next default_gem if default_gem.nonzero?
+ a.base_dir_priority(gem_path) <=> b.base_dir_priority(gem_path)
end
end
@@ -893,23 +859,14 @@ class Gem::Specification < Gem::BasicSpecification
# properly sorted.
def self.add_spec(spec)
- return if _all.include? spec
-
- _all << spec
- stubs << spec
- (@@stubs_by_name[spec.name] ||= []) << spec
-
- _resort!(@@stubs_by_name[spec.name])
- _resort!(stubs)
+ specification_record.add_spec(spec)
end
##
# Removes +spec+ from the known specs.
def self.remove_spec(spec)
- _all.delete spec.to_spec
- stubs.delete spec
- (@@stubs_by_name[spec.name] || []).delete spec
+ specification_record.remove_spec(spec)
end
##
@@ -917,33 +874,23 @@ class Gem::Specification < Gem::BasicSpecification
# You probably want to use one of the Enumerable methods instead.
def self.all
- warn "NOTE: Specification.all called from #{caller.first}" unless
+ warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless
Gem::Deprecate.skip
_all
end
##
- # Sets the known specs to +specs+. Not guaranteed to work for you in
- # the future. Use at your own risk. Caveat emptor. Doomy doom doom.
- # Etc etc.
- #
- #--
- # Makes +specs+ the known specs
- # Listen, time is a river
- # Winter comes, code breaks
- #
- # -- wilsonb
+ # Sets the known specs to +specs+.
def self.all=(specs)
- @@stubs_by_name = specs.group_by(&:name)
- @@all = @@stubs = specs
+ specification_record.all = specs
end
##
# Return full names of all specs in sorted order.
def self.all_names
- _all.map(&:full_name)
+ specification_record.all_names
end
##
@@ -968,9 +915,7 @@ class Gem::Specification < Gem::BasicSpecification
# Return the directories that Specification uses to find specs.
def self.dirs
- @@dirs ||= Gem.path.collect do |dir|
- File.join dir, "specifications"
- end
+ @@dirs ||= Gem::SpecificationRecord.dirs_from(gem_path)
end
##
@@ -980,7 +925,7 @@ class Gem::Specification < Gem::BasicSpecification
def self.dirs=(dirs)
reset
- @@dirs = Array(dirs).map {|dir| File.join dir, "specifications" }
+ @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs))
end
extend Enumerable
@@ -989,21 +934,15 @@ class Gem::Specification < Gem::BasicSpecification
# Enumerate every known spec. See ::dirs= and ::add_spec to set the list of
# specs.
- def self.each
- return enum_for(:each) unless block_given?
-
- _all.each do |x|
- yield x
- end
+ def self.each(&block)
+ specification_record.each(&block)
end
##
# Returns every spec that matches +name+ and optional +requirements+.
def self.find_all_by_name(name, *requirements)
- requirements = Gem::Requirement.default if requirements.empty?
-
- Gem::Dependency.new(name, *requirements).matching_specs
+ specification_record.find_all_by_name(name, *requirements)
end
##
@@ -1033,12 +972,7 @@ class Gem::Specification < Gem::BasicSpecification
# Return the best specification that contains the file matching +path+.
def self.find_by_path(path)
- path = path.dup.freeze
- spec = @@spec_with_requirable_file[path] ||= stubs.find do |s|
- s.contains_requirable_file? path
- end || NOT_FOUND
-
- spec.to_spec
+ specification_record.find_by_path(path)
end
##
@@ -1046,19 +980,15 @@ class Gem::Specification < Gem::BasicSpecification
# amongst the specs that are not activated.
def self.find_inactive_by_path(path)
- stub = stubs.find do |s|
- next if s.activated?
- s.contains_requirable_file? path
- end
- stub&.to_spec
+ specification_record.find_inactive_by_path(path)
end
- def self.find_active_stub_by_path(path)
- stub = @@active_stub_with_requirable_file[path] ||= stubs.find do |s|
- s.activated? && s.contains_requirable_file?(path)
- end || NOT_FOUND
+ ##
+ # Return the best specification that contains the file matching +path+, among
+ # those already activated.
- stub.this
+ def self.find_active_stub_by_path(path)
+ specification_record.find_active_stub_by_path(path)
end
##
@@ -1125,14 +1055,14 @@ class Gem::Specification < Gem::BasicSpecification
# +prerelease+ is true.
def self.latest_specs(prerelease = false)
- _latest_specs Gem::Specification.stubs, prerelease
+ specification_record.latest_specs(prerelease)
end
##
# Return the latest installed spec for gem +name+.
def self.latest_spec_for(name)
- latest_specs(true).find {|installed_spec| installed_spec.name == name }
+ specification_record.latest_spec_for(name)
end
def self._latest_specs(specs, prerelease = false) # :nodoc:
@@ -1270,7 +1200,7 @@ class Gem::Specification < Gem::BasicSpecification
def self.reset
@@dirs = nil
Gem.pre_reset_hooks.each(&:call)
- clear_specs
+ @specification_record = nil
clear_load_cache
unresolved = unresolved_deps
unless unresolved.empty?
@@ -1291,6 +1221,13 @@ class Gem::Specification < Gem::BasicSpecification
Gem.post_reset_hooks.each(&:call)
end
+ ##
+ # Keeps track of all currently known specifications
+
+ def self.specification_record
+ @specification_record ||= Gem::SpecificationRecord.new(dirs)
+ end
+
# DOC: This method needs documented or nodoc'd
def self.unresolved_deps
@unresolved_deps ||= Hash.new {|h, n| h[n] = Gem::Dependency.new n }
@@ -1381,7 +1318,7 @@ class Gem::Specification < Gem::BasicSpecification
spec.instance_variable_set :@has_rdoc, array[15]
spec.instance_variable_set :@new_platform, array[16]
spec.instance_variable_set :@platform, array[16].to_s
- spec.instance_variable_set :@license, array[17]
+ spec.instance_variable_set :@licenses, [array[17]]
spec.instance_variable_set :@metadata, array[18]
spec.instance_variable_set :@loaded, false
spec.instance_variable_set :@activated, false
@@ -1567,7 +1504,7 @@ class Gem::Specification < Gem::BasicSpecification
private :add_dependency_with_type
- alias_method :add_dependency, :add_runtime_dependency
+ alias_method :add_runtime_dependency, :add_dependency
##
# Adds this spec's require paths to LOAD_PATH, in the proper location.
@@ -1976,7 +1913,8 @@ class Gem::Specification < Gem::BasicSpecification
end
##
- # Work around bundler removing my methods
+ # Work around old bundler versions removing my methods
+ # Can be removed once RubyGems can no longer install Bundler 2.5
def gem_dir # :nodoc:
super
@@ -2534,7 +2472,7 @@ class Gem::Specification < Gem::BasicSpecification
if @installed_by_version
result << nil
- result << " s.installed_by_version = #{ruby_code Gem::VERSION} if s.respond_to? :installed_by_version"
+ result << " s.installed_by_version = #{ruby_code Gem::VERSION}"
end
unless dependencies.empty?
@@ -2649,6 +2587,10 @@ class Gem::Specification < Gem::BasicSpecification
@test_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
end
+ def validate_for_resolution
+ Gem::SpecificationPolicy.new(self).validate_for_resolution
+ end
+
def validate_metadata
Gem::SpecificationPolicy.new(self).validate_metadata
end
diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb
index 812b0f889e..d79ee7df92 100644
--- a/lib/rubygems/specification_policy.rb
+++ b/lib/rubygems/specification_policy.rb
@@ -45,6 +45,7 @@ class Gem::SpecificationPolicy
def validate(strict = false)
validate_required!
+ validate_required_metadata!
validate_optional(strict) if packaging || strict
@@ -85,15 +86,17 @@ class Gem::SpecificationPolicy
validate_authors_field
- validate_metadata
-
validate_licenses_length
- validate_lazy_metadata
-
validate_duplicate_dependencies
end
+ def validate_required_metadata!
+ validate_metadata
+
+ validate_lazy_metadata
+ end
+
def validate_optional(strict)
validate_licenses
@@ -121,6 +124,13 @@ class Gem::SpecificationPolicy
end
##
+ # Implementation for Specification#validate_for_resolution
+
+ def validate_for_resolution
+ validate_required!
+ end
+
+ ##
# Implementation for Specification#validate_metadata
def validate_metadata
@@ -297,7 +307,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use:
elsif !VALID_NAME_PATTERN.match?(name)
error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores"
elsif SPECIAL_CHARACTERS.match?(name)
- error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore"
+ error "invalid value for attribute name: #{name.dump} cannot begin with a period, dash, or underscore"
end
end
diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb
new file mode 100644
index 0000000000..664d506265
--- /dev/null
+++ b/lib/rubygems/specification_record.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+
+module Gem
+ class SpecificationRecord
+ def self.dirs_from(paths)
+ paths.map do |path|
+ File.join(path, "specifications")
+ end
+ end
+
+ def self.from_path(path)
+ new(dirs_from([path]))
+ end
+
+ def initialize(dirs)
+ @all = nil
+ @stubs = nil
+ @stubs_by_name = {}
+ @spec_with_requirable_file = {}
+ @active_stub_with_requirable_file = {}
+
+ @dirs = dirs
+ end
+
+ # Sentinel object to represent "not found" stubs
+ NOT_FOUND = Struct.new(:to_spec, :this).new
+ private_constant :NOT_FOUND
+
+ ##
+ # Returns the list of all specifications in the record
+
+ def all
+ @all ||= Gem.loaded_specs.values | stubs.map(&:to_spec)
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+
+ def stubs
+ @stubs ||= begin
+ pattern = "*.gemspec"
+ stubs = stubs_for_pattern(pattern, false)
+
+ @stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name)
+ stubs
+ end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+ # named +name+ only returns stubs that match Gem.platforms
+
+ def stubs_for(name)
+ if @stubs
+ @stubs_by_name[name] || []
+ else
+ @stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s|
+ s.name == name
+ end
+ end
+ end
+
+ ##
+ # Finds stub specifications matching a pattern in the record, optionally
+ # filtering out specs not matching the current platform
+
+ def stubs_for_pattern(pattern, match_platform = true)
+ installed_stubs = installed_stubs(pattern)
+ installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform
+ stubs = installed_stubs + Gem::Specification.default_stubs(pattern)
+ Gem::Specification._resort!(stubs)
+ stubs
+ end
+
+ ##
+ # Adds +spec+ to the the record, keeping the collection properly sorted.
+
+ def add_spec(spec)
+ return if all.include? spec
+
+ all << spec
+ stubs << spec
+ (@stubs_by_name[spec.name] ||= []) << spec
+
+ Gem::Specification._resort!(@stubs_by_name[spec.name])
+ Gem::Specification._resort!(stubs)
+ end
+
+ ##
+ # Removes +spec+ from the record.
+
+ def remove_spec(spec)
+ all.delete spec.to_spec
+ stubs.delete spec
+ (@stubs_by_name[spec.name] || []).delete spec
+ end
+
+ ##
+ # Sets the specs known by the record to +specs+.
+
+ def all=(specs)
+ @stubs_by_name = specs.group_by(&:name)
+ @all = @stubs = specs
+ end
+
+ ##
+ # Return full names of all specs in the record in sorted order.
+
+ def all_names
+ all.map(&:full_name)
+ end
+
+ include Enumerable
+
+ ##
+ # Enumerate every known spec.
+
+ def each
+ return enum_for(:each) unless block_given?
+
+ all.each do |x|
+ yield x
+ end
+ end
+
+ ##
+ # Returns every spec in the record that matches +name+ and optional +requirements+.
+
+ def find_all_by_name(name, *requirements)
+ req = Gem::Requirement.create(*requirements)
+ env_req = Gem.env_requirement(name)
+
+ matches = stubs_for(name).find_all do |spec|
+ req.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version)
+ end.map(&:to_spec)
+
+ if name == "bundler" && !req.specific?
+ require_relative "bundler_version_finder"
+ Gem::BundlerVersionFinder.prioritize!(matches)
+ end
+
+ matches
+ end
+
+ ##
+ # Return the best specification in the record that contains the file matching +path+.
+
+ def find_by_path(path)
+ path = path.dup.freeze
+ spec = @spec_with_requirable_file[path] ||= stubs.find do |s|
+ s.contains_requirable_file? path
+ end || NOT_FOUND
+
+ spec.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+ amongst the specs that are not activated.
+
+ def find_inactive_by_path(path)
+ stub = stubs.find do |s|
+ next if s.activated?
+ s.contains_requirable_file? path
+ end
+ stub&.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+, among those already activated.
+
+ def find_active_stub_by_path(path)
+ stub = @active_stub_with_requirable_file[path] ||= stubs.find do |s|
+ s.activated? && s.contains_requirable_file?(path)
+ end || NOT_FOUND
+
+ stub.this
+ end
+
+ ##
+ # Return the latest specs in the record, optionally including prerelease
+ # specs if +prerelease+ is true.
+
+ def latest_specs(prerelease)
+ Gem::Specification._latest_specs stubs, prerelease
+ end
+
+ ##
+ # Return the latest installed spec in the record for gem +name+.
+
+ def latest_spec_for(name)
+ latest_specs(true).find {|installed_spec| installed_spec.name == name }
+ end
+
+ private
+
+ def installed_stubs(pattern)
+ map_stubs(pattern) do |path, base_dir, gems_dir|
+ Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir)
+ end
+ end
+
+ def map_stubs(pattern)
+ @dirs.flat_map do |dir|
+ base_dir = File.dirname dir
+ gems_dir = File.join base_dir, "gems"
+ Gem::Specification.gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir }
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb
index 58748df5d6..ea66fbc3f6 100644
--- a/lib/rubygems/stub_specification.rb
+++ b/lib/rubygems/stub_specification.rb
@@ -210,4 +210,25 @@ class Gem::StubSpecification < Gem::BasicSpecification
def stubbed?
data.is_a? StubLine
end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ name == other.name &&
+ version == other.version &&
+ platform == other.platform
+ end
+
+ alias_method :eql?, :== # :nodoc:
+
+ def hash # :nodoc:
+ name.hash ^ version.hash ^ platform.hash
+ end
+
+ def <=>(other) # :nodoc:
+ sort_obj <=> other.sort_obj
+ end
+
+ def sort_obj # :nodoc:
+ [name, version, Gem::Platform.sort_priority(platform)]
+ end
end
diff --git a/lib/rubygems/target_rbconfig.rb b/lib/rubygems/target_rbconfig.rb
new file mode 100644
index 0000000000..21d90ee9db
--- /dev/null
+++ b/lib/rubygems/target_rbconfig.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+##
+# A TargetConfig is a wrapper around an RbConfig object that provides a
+# consistent interface for querying configuration for *deployment target
+# platform*, where the gem being installed is intended to run on.
+#
+# The TargetConfig is typically created from the RbConfig of the running Ruby
+# process, but can also be created from an RbConfig file on disk for cross-
+# compiling gems.
+
+class Gem::TargetRbConfig
+ attr_reader :path
+
+ def initialize(rbconfig, path)
+ @rbconfig = rbconfig
+ @path = path
+ end
+
+ ##
+ # Creates a TargetRbConfig for the platform that RubyGems is running on.
+
+ def self.for_running_ruby
+ new(::RbConfig, nil)
+ end
+
+ ##
+ # Creates a TargetRbConfig from the RbConfig file at the given path.
+ # Typically used for cross-compiling gems.
+
+ def self.from_path(rbconfig_path)
+ namespace = Module.new do |m|
+ # Load the rbconfig.rb file within a new anonymous module to avoid
+ # conflicts with the rbconfig for the running platform.
+ Kernel.load rbconfig_path, m
+ end
+ rbconfig = namespace.const_get(:RbConfig)
+
+ new(rbconfig, rbconfig_path)
+ end
+
+ ##
+ # Queries the configuration for the given key.
+
+ def [](key)
+ @rbconfig::CONFIG[key]
+ end
+end
diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
index f4202caa0a..471c29b6e4 100644
--- a/lib/rubygems/uninstaller.rb
+++ b/lib/rubygems/uninstaller.rb
@@ -32,7 +32,7 @@ class Gem::Uninstaller
attr_reader :bin_dir
##
- # The gem repository the gem will be installed into
+ # The gem repository the gem will be uninstalled from
attr_reader :gem_home
@@ -49,7 +49,9 @@ class Gem::Uninstaller
# TODO: document the valid options
@gem = gem
@version = options[:version] || Gem::Requirement.default
- @gem_home = File.realpath(options[:install_dir] || Gem.dir)
+ @install_dir = options[:install_dir]
+ @gem_home = File.realpath(@install_dir || Gem.dir)
+ @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir
@force_executables = options[:executables]
@force_all = options[:all]
@force_ignore = options[:ignore]
@@ -69,7 +71,7 @@ class Gem::Uninstaller
# only add user directory if install_dir is not set
@user_install = false
- @user_install = options[:user_install] unless options[:install_dir]
+ @user_install = options[:user_install] unless @install_dir
# Optimization: populated during #uninstall
@default_specs_matching_uninstall_params = []
@@ -84,11 +86,7 @@ class Gem::Uninstaller
list = []
- dirs =
- Gem::Specification.dirs +
- [Gem.default_specifications_dir]
-
- Gem::Specification.each_spec dirs do |spec|
+ specification_record.stubs.each do |spec|
next unless dependency.matches_spec? spec
list << spec
@@ -100,11 +98,11 @@ class Gem::Uninstaller
default_specs, list = list.partition(&:default_gem?)
warn_cannot_uninstall_default_gems(default_specs - list)
- @default_specs_matching_uninstall_params = default_specs
+ @default_specs_matching_uninstall_params = default_specs.map(&:to_spec)
list, other_repo_specs = list.partition do |spec|
@gem_home == spec.base_dir ||
- (@user_install && spec.base_dir == Gem.user_dir)
+ (@user_install && spec.base_dir == @user_dir)
end
list.sort!
@@ -124,7 +122,7 @@ class Gem::Uninstaller
remove_all list
elsif list.size > 1
- gem_names = list.map(&:full_name)
+ gem_names = list.map(&:full_name_with_location)
gem_names << "All versions"
say
@@ -145,7 +143,9 @@ class Gem::Uninstaller
##
# Uninstalls gem +spec+
- def uninstall_gem(spec)
+ def uninstall_gem(stub)
+ spec = stub.to_spec
+
@spec = spec
unless dependencies_ok? spec
@@ -163,6 +163,8 @@ class Gem::Uninstaller
remove_plugins @spec
remove @spec
+ specification_record.remove_spec(stub)
+
regenerate_plugins
Gem.post_uninstall_hooks.each do |hook|
@@ -176,7 +178,7 @@ class Gem::Uninstaller
# Removes installed executables and batch files (windows only) for +spec+.
def remove_executables(spec)
- return if spec.executables.empty?
+ return if spec.executables.empty? || default_spec_matches?(spec)
executables = spec.executables.clone
@@ -238,7 +240,7 @@ class Gem::Uninstaller
def remove(spec)
unless path_ok?(@gem_home, spec) ||
- (@user_install && path_ok?(Gem.user_dir, spec))
+ (@user_install && path_ok?(@user_dir, spec))
e = Gem::GemNotInHomeException.new \
"Gem '#{spec.full_name}' is not installed in directory #{@gem_home}"
e.spec = spec
@@ -249,7 +251,15 @@ class Gem::Uninstaller
raise Gem::FilePermissionError, spec.base_dir unless
File.writable?(spec.base_dir)
- safe_delete { FileUtils.rm_r spec.full_gem_path }
+ full_gem_path = spec.full_gem_path
+ exclusions = []
+
+ if default_spec_matches?(spec) && spec.executables.any?
+ exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) }
+ exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path
+ end
+
+ safe_delete { rm_r full_gem_path, exclusions: exclusions }
safe_delete { FileUtils.rm_r spec.extension_dir }
old_platform_name = spec.original_name
@@ -273,8 +283,6 @@ class Gem::Uninstaller
safe_delete { FileUtils.rm_r gemspec }
announce_deletion_of(spec)
-
- Gem::Specification.reset
end
##
@@ -290,7 +298,7 @@ class Gem::Uninstaller
# Regenerates plugin wrappers after removal.
def regenerate_plugins
- latest = Gem::Specification.latest_spec_for(@spec.name)
+ latest = specification_record.latest_spec_for(@spec.name)
return if latest.nil?
regenerate_plugins_for(latest, plugin_dir_for(@spec))
@@ -378,6 +386,16 @@ class Gem::Uninstaller
private
+ def rm_r(path, exclusions:)
+ FileUtils::Entry_.new(path).postorder_traverse do |ent|
+ ent.remove unless exclusions.include?(ent.path)
+ end
+ end
+
+ def specification_record
+ @specification_record ||= @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record
+ end
+
def announce_deletion_of(spec)
name = spec.full_name
say "Successfully uninstalled #{name}"
diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb
index f3c7201639..192ae30b9b 100644
--- a/lib/rubygems/util/licenses.rb
+++ b/lib/rubygems/util/licenses.rb
@@ -15,6 +15,7 @@ class Gem::Licenses
# license identifiers
LICENSE_IDENTIFIERS = %w[
0BSD
+ 3D-Slicer-1.0
AAL
ADSL
AFL-1.1
@@ -26,6 +27,7 @@ class Gem::Licenses
AGPL-1.0-or-later
AGPL-3.0-only
AGPL-3.0-or-later
+ AMD-newlib
AMDPLPA
AML
AML-glslang
@@ -62,6 +64,7 @@ class Gem::Licenses
BSD-2-Clause-Darwin
BSD-2-Clause-Patent
BSD-2-Clause-Views
+ BSD-2-Clause-first-lines
BSD-3-Clause
BSD-3-Clause-Attribution
BSD-3-Clause-Clear
@@ -191,6 +194,7 @@ class Gem::Licenses
CUA-OPL-1.0
Caldera
Caldera-no-preamble
+ Catharon
ClArtistic
Clips
Community-Spec-1.0
@@ -270,25 +274,32 @@ class Gem::Licenses
Glide
Glulxe
Graphics-Gems
+ Gutmann
HP-1986
HP-1989
HPND
HPND-DEC
HPND-Fenneberg-Livingston
HPND-INRIA-IMAG
+ HPND-Intel
HPND-Kevlin-Henney
HPND-MIT-disclaimer
HPND-Markus-Kuhn
HPND-Pbmplus
HPND-UC
+ HPND-UC-export-US
HPND-doc
HPND-doc-sell
HPND-export-US
+ HPND-export-US-acknowledgement
HPND-export-US-modify
+ HPND-export2-US
+ HPND-merchantability-variant
HPND-sell-MIT-disclaimer-xserver
HPND-sell-regexpr
HPND-sell-variant
HPND-sell-variant-MIT-disclaimer
+ HPND-sell-variant-MIT-disclaimer-rev
HTMLTIDY
HaskellReport
Hippocratic-2.1
@@ -353,6 +364,7 @@ class Gem::Licenses
MIT-0
MIT-CMU
MIT-Festival
+ MIT-Khronos-old
MIT-Modern-Variant
MIT-Wu
MIT-advertising
@@ -386,7 +398,9 @@ class Gem::Licenses
NAIST-2003
NASA-1.3
NBPL-1.0
+ NCBI-PD
NCGL-UK-2.0
+ NCL
NCSA
NGPL
NICTA-1.0
@@ -410,6 +424,7 @@ class Gem::Licenses
Nokia
Noweb
O-UDA-1.0
+ OAR
OCCT-PL
OCLC-2.0
ODC-By-1.0
@@ -463,6 +478,7 @@ class Gem::Licenses
PDDL-1.0
PHP-3.0
PHP-3.01
+ PPL
PSF-2.0
Parity-6.0.0
Parity-7.0.0
@@ -518,6 +534,7 @@ class Gem::Licenses
Spencer-99
SugarCRM-1.1.3
Sun-PPP
+ Sun-PPP-2000
SunPro
Symlinks
TAPR-OHL-1.0
@@ -574,6 +591,7 @@ class Gem::Licenses
Zimbra-1.3
Zimbra-1.4
Zlib
+ any-OSI
bcrypt-Solar-Designer
blessing
bzip2-1.0.6
@@ -582,6 +600,7 @@ class Gem::Licenses
copyleft-next-0.3.0
copyleft-next-0.3.1
curl
+ cve-tou
diffmark
dtoa
dvipdfm
@@ -604,6 +623,7 @@ class Gem::Licenses
mpi-permissive
mpich2
mplus
+ pkgconf
pnmstitch
psfrag
psutils
@@ -613,12 +633,14 @@ class Gem::Licenses
softSurfer
ssh-keyscan
swrule
+ threeparttable
ulem
w3m
xinetd
xkeyboard-config-Zinoviev
xlock
xpp
+ xzoom
zlib-acknowledgement
].freeze
@@ -660,6 +682,7 @@ class Gem::Licenses
EXCEPTION_IDENTIFIERS = %w[
389-exception
Asterisk-exception
+ Asterisk-linking-protocols-exception
Autoconf-exception-2.0
Autoconf-exception-3.0
Autoconf-exception-generic
@@ -697,11 +720,13 @@ class Gem::Licenses
OCCT-exception-1.0
OCaml-LGPL-linking-exception
OpenJDK-assembly-exception-1.0
+ PCRE2-exception
PS-or-PDF-font-exception-20170817
QPL-1.0-INRIA-2004-exception
Qt-GPL-exception-1.0
Qt-LGPL-exception-1.1
Qwt-exception-1.0
+ RRDtool-FLOSS-exception-2.0
SANE-exception
SHL-2.0
SHL-2.1
diff --git a/lib/rubygems/vendor/net-http/lib/net/https.rb b/lib/rubygems/vendor/net-http/lib/net/https.rb
index d2784f0be0..f104c85c81 100644
--- a/lib/rubygems/vendor/net-http/lib/net/https.rb
+++ b/lib/rubygems/vendor/net-http/lib/net/https.rb
@@ -4,7 +4,7 @@
= net/https -- SSL/TLS enhancement for Gem::Net::HTTP.
This file has been merged with net/http. There is no longer any need to
- require 'rubygems/vendor/net-http/lib/net/https' to use HTTPS.
+ require_relative 'https' to use HTTPS.
See Gem::Net::HTTP for details on how to make HTTPS connections.
diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb
index 5937431720..00dc7c8a67 100644
--- a/lib/rubygems/vendor/optparse/lib/optparse.rb
+++ b/lib/rubygems/vendor/optparse/lib/optparse.rb
@@ -1084,7 +1084,7 @@ XXX
Switch::OptionalArgument.new do |pkg|
if pkg
begin
- require 'rubygems/vendor/optparse/lib/optparse/version'
+ require_relative 'optparse/version'
rescue LoadError
else
show_version(*pkg.split(/,/)) or
diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb
index ac0ba0b313..0f5ded3b76 100644
--- a/lib/rubygems/vendor/resolv/lib/resolv.rb
+++ b/lib/rubygems/vendor/resolv/lib/resolv.rb
@@ -5,7 +5,7 @@ require_relative '../../timeout/lib/timeout'
require 'io/wait'
begin
- require 'securerandom'
+ require_relative '../../../vendored_securerandom'
rescue LoadError
end
@@ -602,10 +602,10 @@ class Gem::Resolv
}
end
- if defined? SecureRandom
+ if defined? Gem::SecureRandom
def self.random(arg) # :nodoc:
begin
- SecureRandom.random_number(arg)
+ Gem::SecureRandom.random_number(arg)
rescue NotImplementedError
rand(arg)
end
diff --git a/lib/rubygems/vendor/securerandom/.document b/lib/rubygems/vendor/securerandom/.document
new file mode 100644
index 0000000000..0c43bbd6b3
--- /dev/null
+++ b/lib/rubygems/vendor/securerandom/.document
@@ -0,0 +1 @@
+# Vendored files do not need to be documented
diff --git a/lib/rubygems/vendor/securerandom/lib/random/formatter.rb b/lib/rubygems/vendor/securerandom/lib/random/formatter.rb
new file mode 100644
index 0000000000..3544033340
--- /dev/null
+++ b/lib/rubygems/vendor/securerandom/lib/random/formatter.rb
@@ -0,0 +1,373 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+# == \Random number formatter.
+#
+# Formats generated random numbers in many manners. When <tt>'random/formatter'</tt>
+# is required, several methods are added to empty core module <tt>Gem::Random::Formatter</tt>,
+# making them available as Random's instance and module methods.
+#
+# Standard library Gem::SecureRandom is also extended with the module, and the methods
+# described below are available as a module methods in it.
+#
+# === Examples
+#
+# Generate random hexadecimal strings:
+#
+# require 'rubygems/vendor/securerandom/lib/random/formatter'
+#
+# prng = Random.new
+# prng.hex(10) #=> "52750b30ffbc7de3b362"
+# prng.hex(10) #=> "92b15d6c8dc4beb5f559"
+# prng.hex(13) #=> "39b290146bea6ce975c37cfc23"
+# # or just
+# Random.hex #=> "1aed0c631e41be7f77365415541052ee"
+#
+# Generate random base64 strings:
+#
+# prng.base64(10) #=> "EcmTPZwWRAozdA=="
+# prng.base64(10) #=> "KO1nIU+p9DKxGg=="
+# prng.base64(12) #=> "7kJSM/MzBJI+75j8"
+# Random.base64(4) #=> "bsQ3fQ=="
+#
+# Generate random binary strings:
+#
+# prng.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
+# prng.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
+# Random.random_bytes(6) #=> "\xA1\xE6Lr\xC43"
+#
+# Generate alphanumeric strings:
+#
+# prng.alphanumeric(10) #=> "S8baxMJnPl"
+# prng.alphanumeric(10) #=> "aOxAg8BAJe"
+# Random.alphanumeric #=> "TmP9OsJHJLtaZYhP"
+#
+# Generate UUIDs:
+#
+# prng.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+# prng.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+# Random.uuid #=> "f14e0271-de96-45cc-8911-8910292a42cd"
+#
+# All methods are available in the standard library Gem::SecureRandom, too:
+#
+# Gem::SecureRandom.hex #=> "05b45376a30c67238eb93b16499e50cf"
+
+module Gem::Random::Formatter
+
+ # Generate a random binary string.
+ #
+ # The argument _n_ specifies the length of the result string.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in future.
+ #
+ # The result may contain any byte: "\x00" - "\xff".
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
+ # # or
+ # prng = Random.new
+ # prng.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
+ def random_bytes(n=nil)
+ n = n ? n.to_int : 16
+ gen_random(n)
+ end
+
+ # Generate a random hexadecimal string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number to be generated.
+ # The length of the resulting hexadecimal string is twice of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain 0-9 and a-f.
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
+ # # or
+ # prng = Random.new
+ # prng.hex #=> "91dc3bfb4de5b11d029d376634589b61"
+ def hex(n=nil)
+ random_bytes(n).unpack1("H*")
+ end
+
+ # Generate a random base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
+ # # or
+ # prng = Random.new
+ # prng.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
+ #
+ # See RFC 3548 for the definition of base64.
+ def base64(n=nil)
+ [random_bytes(n)].pack("m0")
+ end
+
+ # Generate a random URL-safe base64 string.
+ #
+ # The argument _n_ specifies the length, in bytes, of the random number
+ # to be generated. The length of the result string is about 4/3 of _n_.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The boolean argument _padding_ specifies the padding.
+ # If it is false or nil, padding is not generated.
+ # Otherwise padding is generated.
+ # By default, padding is not generated because "=" may be used as a URL delimiter.
+ #
+ # The result may contain A-Z, a-z, 0-9, "-" and "_".
+ # "=" is also used if _padding_ is true.
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
+ # # or
+ # prng = Random.new
+ # prng.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
+ #
+ # prng.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
+ # prng.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
+ #
+ # See RFC 3548 for the definition of URL-safe base64.
+ def urlsafe_base64(n=nil, padding=false)
+ s = [random_bytes(n)].pack("m0")
+ s.tr!("+/", "-_")
+ s.delete!("=") unless padding
+ s
+ end
+
+ # Generate a random v4 UUID (Universally Unique IDentifier).
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
+ # Random.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
+ # # or
+ # prng = Random.new
+ # prng.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
+ #
+ # The version 4 UUID is purely random (except the version).
+ # It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
+ #
+ # The result contains 122 random bits (15.25 random bytes).
+ #
+ # See RFC4122[https://datatracker.ietf.org/doc/html/rfc4122] for details of UUID.
+ #
+ def uuid
+ ary = random_bytes(16).unpack("NnnnnN")
+ ary[2] = (ary[2] & 0x0fff) | 0x4000
+ ary[3] = (ary[3] & 0x3fff) | 0x8000
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
+ end
+
+ alias uuid_v4 uuid
+
+ # Generate a random v7 UUID (Universally Unique IDentifier).
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.uuid_v7 # => "0188d4c3-1311-7f96-85c7-242a7aa58f1e"
+ # Random.uuid_v7 # => "0188d4c3-16fe-744f-86af-38fa04c62bb5"
+ # Random.uuid_v7 # => "0188d4c3-1af8-764f-b049-c204ce0afa23"
+ # Random.uuid_v7 # => "0188d4c3-1e74-7085-b14f-ef6415dc6f31"
+ # # |<--sorted-->| |<----- random ---->|
+ #
+ # # or
+ # prng = Random.new
+ # prng.uuid_v7 # => "0188ca51-5e72-7950-a11d-def7ff977c98"
+ #
+ # The version 7 UUID starts with the least significant 48 bits of a 64 bit
+ # Unix timestamp (milliseconds since the epoch) and fills the remaining bits
+ # with random data, excluding the version and variant bits.
+ #
+ # This allows version 7 UUIDs to be sorted by creation time. Time ordered
+ # UUIDs can be used for better database index locality of newly inserted
+ # records, which may have a significant performance benefit compared to random
+ # data inserts.
+ #
+ # The result contains 74 random bits (9.25 random bytes).
+ #
+ # Note that this method cannot be made reproducable because its output
+ # includes not only random bits but also timestamp.
+ #
+ # See draft-ietf-uuidrev-rfc4122bis[https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/]
+ # for details of UUIDv7.
+ #
+ # ==== Monotonicity
+ #
+ # UUIDv7 has millisecond precision by default, so multiple UUIDs created
+ # within the same millisecond are not issued in monotonically increasing
+ # order. To create UUIDs that are time-ordered with sub-millisecond
+ # precision, up to 12 bits of additional timestamp may added with
+ # +extra_timestamp_bits+. The extra timestamp precision comes at the expense
+ # of random bits. Setting <tt>extra_timestamp_bits: 12</tt> provides ~244ns
+ # of precision, but only 62 random bits (7.75 random bytes).
+ #
+ # prng = Random.new
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 12) }
+ # # =>
+ # ["0188d4c7-13da-74f9-8b53-22a786ffdd5a",
+ # "0188d4c7-13da-753b-83a5-7fb9b2afaeea",
+ # "0188d4c7-13da-754a-88ea-ac0baeedd8db",
+ # "0188d4c7-13da-7557-83e1-7cad9cda0d8d"]
+ # # |<--- sorted --->| |<-- random --->|
+ #
+ # Array.new(4) { prng.uuid_v7(extra_timestamp_bits: 8) }
+ # # =>
+ # ["0188d4c7-3333-7a95-850a-de6edb858f7e",
+ # "0188d4c7-3333-7ae8-842e-bc3a8b7d0cf9", # <- out of order
+ # "0188d4c7-3333-7ae2-995a-9f135dc44ead", # <- out of order
+ # "0188d4c7-3333-7af9-87c3-8f612edac82e"]
+ # # |<--- sorted -->||<---- random --->|
+ #
+ # Any rollbacks of the system clock will break monotonicity. UUIDv7 is based
+ # on UTC, which excludes leap seconds and can rollback the clock. To avoid
+ # this, the system clock can synchronize with an NTP server configured to use
+ # a "leap smear" approach. NTP or PTP will also be needed to synchronize
+ # across distributed nodes.
+ #
+ # Counters and other mechanisms for stronger guarantees of monotonicity are
+ # not implemented. Applications with stricter requirements should follow
+ # {Section 6.2}[https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-07.html#monotonicity_counters]
+ # of the specification.
+ #
+ def uuid_v7(extra_timestamp_bits: 0)
+ case (extra_timestamp_bits = Integer(extra_timestamp_bits))
+ when 0 # min timestamp precision
+ ms = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
+ rand = random_bytes(10)
+ rand.setbyte(0, rand.getbyte(0) & 0x0f | 0x70) # version
+ rand.setbyte(2, rand.getbyte(2) & 0x3f | 0x80) # variant
+ "%08x-%04x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ rand.unpack("H4H4H12").join("-")
+ ]
+
+ when 12 # max timestamp precision
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ extra_bits = ns * 4096 / 1_000_000
+ rand = random_bytes(8)
+ rand.setbyte(0, rand.getbyte(0) & 0x3f | 0x80) # variant
+ "%08x-%04x-7%03x-%s" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ extra_bits,
+ rand.unpack("H4H12").join("-")
+ ]
+
+ when (0..12) # the generic version is slower than the special cases above
+ rand_a, rand_b1, rand_b2, rand_b3 = random_bytes(10).unpack("nnnN")
+ rand_mask_bits = 12 - extra_timestamp_bits
+ ms, ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
+ .divmod(1_000_000)
+ "%08x-%04x-%04x-%04x-%04x%08x" % [
+ (ms & 0x0000_ffff_ffff_0000) >> 16,
+ (ms & 0x0000_0000_0000_ffff),
+ 0x7000 |
+ ((ns * (1 << extra_timestamp_bits) / 1_000_000) << rand_mask_bits) |
+ rand_a & ((1 << rand_mask_bits) - 1),
+ 0x8000 | (rand_b1 & 0x3fff),
+ rand_b2,
+ rand_b3
+ ]
+
+ else
+ raise ArgumentError, "extra_timestamp_bits must be in 0..12"
+ end
+ end
+
+ # Internal interface to Random; Generate random data _n_ bytes.
+ private def gen_random(n)
+ self.bytes(n)
+ end
+
+ # Generate a string that randomly draws from a
+ # source array of characters.
+ #
+ # The argument _source_ specifies the array of characters from which
+ # to generate the string.
+ # The argument _n_ specifies the length, in characters, of the string to be
+ # generated.
+ #
+ # The result may contain whatever characters are in the source array.
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # prng.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
+ # prng.choose([*'0'..'9'], 5) #=> "27309"
+ private def choose(source, n)
+ size = source.size
+ m = 1
+ limit = size
+ while limit * size <= 0x100000000
+ limit *= size
+ m += 1
+ end
+ result = ''.dup
+ while m <= n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ (m-is.length).times { is << 0 }
+ result << source.values_at(*is).join('')
+ n -= m
+ end
+ if 0 < n
+ rs = random_number(limit)
+ is = rs.digits(size)
+ if is.length < n
+ (n-is.length).times { is << 0 }
+ else
+ is.pop while n < is.length
+ end
+ result.concat source.values_at(*is).join('')
+ end
+ result
+ end
+
+ # The default character list for #alphanumeric.
+ ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9']
+
+ # Generate a random alphanumeric string.
+ #
+ # The argument _n_ specifies the length, in characters, of the alphanumeric
+ # string to be generated.
+ # The argument _chars_ specifies the character list which the result is
+ # consist of.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed.
+ # It may be larger in the future.
+ #
+ # The result may contain A-Z, a-z and 0-9, unless _chars_ is specified.
+ #
+ # require 'rubygems/vendor/securerandom/lib/random/formatter'
+ #
+ # Random.alphanumeric #=> "2BuBuLf3WfSKyQbR"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10) #=> "i6K93NdqiH"
+ #
+ # Random.alphanumeric(4, chars: [*"0".."9"]) #=> "2952"
+ # # or
+ # prng = Random.new
+ # prng.alphanumeric(10, chars: [*"!".."/"]) #=> ",.,++%/''."
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end
+end
diff --git a/lib/rubygems/vendor/securerandom/lib/securerandom.rb b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
new file mode 100644
index 0000000000..f83d2a74fc
--- /dev/null
+++ b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
@@ -0,0 +1,96 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require_relative 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
+#
+# You can use this library in your application by requiring it:
+#
+# require 'rubygems/vendor/securerandom/lib/securerandom'
+#
+# It supports the following secure random number generators:
+#
+# * openssl
+# * /dev/urandom
+# * Win32
+#
+# Gem::SecureRandom is extended by the Gem::Random::Formatter module which
+# defines the following methods:
+#
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
+#
+# These methods are usable as class methods of Gem::SecureRandom such as
+# +Gem::SecureRandom.hex+.
+#
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
+
+module Gem::SecureRandom
+
+ # The version
+ VERSION = "0.3.1"
+
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
+ end
+
+ private
+
+ # :stopdoc:
+
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
+
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
+ end
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
+ end
+ ret
+ end
+
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
+ begin
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
+ end
+
+ # :startdoc:
+
+ # Generate random data bytes for Gem::Random::Formatter
+ public :gen_random
+ end
+end
+
+Gem::SecureRandom.extend(Gem::Random::Formatter)
diff --git a/lib/rubygems/vendored_securerandom.rb b/lib/rubygems/vendored_securerandom.rb
new file mode 100644
index 0000000000..0ce26905c4
--- /dev/null
+++ b/lib/rubygems/vendored_securerandom.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module Gem::Random; end
+require_relative "vendor/securerandom/lib/securerandom"
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb
index e174d8ad95..d9cd91bffa 100644
--- a/lib/rubygems/version.rb
+++ b/lib/rubygems/version.rb
@@ -297,10 +297,6 @@ class Gem::Version
@hash = nil
end
- def to_yaml_properties # :nodoc:
- ["@version"]
- end
-
def encode_with(coder) # :nodoc:
coder.add "version", @version
end
diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb
index 128becc1ce..f89004f32a 100644
--- a/lib/rubygems/yaml_serializer.rb
+++ b/lib/rubygems/yaml_serializer.rb
@@ -41,7 +41,7 @@ module Gem
HASH_REGEX = /
^
([ ]*) # indentations
- (.+) # key
+ ([^#]+) # key excludes comment char '#'
(?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value)
[ ]?
(['"]?) # optional opening quote
@@ -60,7 +60,6 @@ module Gem
indent, key, quote, val = match.captures
val = strip_comment(val)
- convert_to_backward_compatible_key!(key)
depth = indent.size / 2
if quote.empty? && val.empty?
new_hash = {}
@@ -92,14 +91,8 @@ module Gem
end
end
- # for settings' keys
- def convert_to_backward_compatible_key!(key)
- key << "/" if /https?:/i.match?(key) && !%r{/\Z}.match?(key)
- key.gsub!(".", "__")
- end
-
class << self
- private :dump_hash, :convert_to_backward_compatible_key!
+ private :dump_hash
end
end
end
diff --git a/lib/set.rb b/lib/set.rb
index a0954a31d1..8ac9ce45b1 100644
--- a/lib/set.rb
+++ b/lib/set.rb
@@ -3,7 +3,7 @@
#
# set.rb - defines the Set class
#
-# Copyright (c) 2002-2023 Akinori MUSHA <knu@iDaemons.org>
+# Copyright (c) 2002-2024 Akinori MUSHA <knu@iDaemons.org>
#
# Documentation by Akinori MUSHA and Gavin Sinclair.
#
@@ -335,7 +335,7 @@ class Set
end
end
- # Converts the set to an array. The order of elements is uncertain.
+ # Returns an array containing all elements in the set.
#
# Set[1, 2].to_a #=> [1, 2]
# Set[1, 'c', :s].to_a #=> [1, "c", :s]
@@ -540,22 +540,22 @@ class Set
# Deletes every element of the set for which block evaluates to
# true, and returns self. Returns an enumerator if no block is
# given.
- def delete_if
+ def delete_if(&block)
block_given? or return enum_for(__method__) { size }
- # @hash.delete_if should be faster, but using it breaks the order
- # of enumeration in subclasses.
- select { |o| yield o }.each { |o| @hash.delete(o) }
+ # Instead of directly using @hash.delete_if, perform enumeration
+ # using self.each that subclasses may override.
+ select(&block).each { |o| @hash.delete(o) }
self
end
# Deletes every element of the set for which block evaluates to
# false, and returns self. Returns an enumerator if no block is
# given.
- def keep_if
+ def keep_if(&block)
block_given? or return enum_for(__method__) { size }
- # @hash.keep_if should be faster, but using it breaks the order of
- # enumeration in subclasses.
- reject { |o| yield o }.each { |o| @hash.delete(o) }
+ # Instead of directly using @hash.keep_if, perform enumeration
+ # using self.each that subclasses may override.
+ reject(&block).each { |o| @hash.delete(o) }
self
end
diff --git a/lib/shellwords.gemspec b/lib/shellwords.gemspec
index d60ab5f650..8d0c518ca5 100644
--- a/lib/shellwords.gemspec
+++ b/lib/shellwords.gemspec
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ srcdir, gemspec_file = File.split(__FILE__)
+ spec.files = Dir.chdir(srcdir) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:(?:test|spec|features)/|\.git|Rake)}) || f == gemspec_file}
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
diff --git a/lib/shellwords.rb b/lib/shellwords.rb
index 067432e374..d8243abd61 100644
--- a/lib/shellwords.rb
+++ b/lib/shellwords.rb
@@ -5,9 +5,9 @@
# This module manipulates strings according to the word parsing rules
# of the UNIX Bourne shell.
#
-# The shellwords() function was originally a port of shellwords.pl,
-# but modified to conform to the Shell & Utilities volume of the IEEE
-# Std 1003.1-2008, 2016 Edition [1].
+# The <tt>shellwords()</tt> function was originally a port of shellwords.pl, but
+# modified to conform to {the Shell & Utilities volume of the IEEE Std 1003.1-2008, 2016
+# Edition}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html]
#
# === Usage
#
@@ -62,12 +62,9 @@
#
# === Contact
# * Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-#
-# === Resources
-#
-# 1: {IEEE Std 1003.1-2008, 2016 Edition, the Shell & Utilities volume}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html]
module Shellwords
+ # The version number string.
VERSION = "0.2.0"
# Splits a string into an array of tokens in the same way the UNIX
diff --git a/lib/tempfile.rb b/lib/tempfile.rb
index 1d7b80a74d..2f8a39cfe5 100644
--- a/lib/tempfile.rb
+++ b/lib/tempfile.rb
@@ -8,18 +8,61 @@
require 'delegate'
require 'tmpdir'
-# A utility class for managing temporary files. When you create a Tempfile
-# object, it will create a temporary file with a unique filename. A Tempfile
-# objects behaves just like a File object, and you can perform all the usual
-# file operations on it: reading data, writing data, changing its permissions,
-# etc. So although this class does not explicitly document all instance methods
-# supported by File, you can in fact call any File instance method on a
-# Tempfile object.
+# A utility class for managing temporary files.
+#
+# There are two kind of methods of creating a temporary file:
+#
+# - Tempfile.create (recommended)
+# - Tempfile.new and Tempfile.open (mostly for backward compatibility, not recommended)
+#
+# Tempfile.create creates a usual \File object.
+# The timing of file deletion is predictable.
+# Also, it supports open-and-unlink technique which
+# removes the temporary file immediately after creation.
+#
+# Tempfile.new and Tempfile.open creates a \Tempfile object.
+# The created file is removed by the GC (finalizer).
+# The timing of file deletion is not predictable.
#
# == Synopsis
#
# require 'tempfile'
#
+# # Tempfile.create with a block
+# # The filename are choosen automatically.
+# # (You can specify the prefix and suffix of the filename by an optional argument.)
+# Tempfile.create {|f|
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# } # The file is removed at block exit.
+#
+# # Tempfile.create without a block
+# # You need to unlink the file in non-block form.
+# f = Tempfile.create
+# f.puts "foo"
+# f.close
+# File.unlink(f.path) # You need to unlink the file.
+#
+# # Tempfile.create(anonymous: true) without a block
+# f = Tempfile.create(anonymous: true)
+# # The file is already removed because anonymous.
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.close
+#
+# # Tempfile.create(anonymous: true) with a block
+# Tempfile.create(anonymous: true) {|f|
+# # The file is already removed because anonymous.
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# }
+#
+# # Not recommended: Tempfile.new without a block
# file = Tempfile.new('foo')
# file.path # => A unique filename in the OS's temp directory,
# # e.g.: "/tmp/foo.24722.0"
@@ -30,7 +73,27 @@ require 'tmpdir'
# file.close
# file.unlink # deletes the temp file
#
-# == Good practices
+# == About Tempfile.new and Tempfile.open
+#
+# This section does not apply to Tempfile.create because
+# it returns a File object (not a Tempfile object).
+#
+# When you create a Tempfile object,
+# it will create a temporary file with a unique filename. A Tempfile
+# objects behaves just like a File object, and you can perform all the usual
+# file operations on it: reading data, writing data, changing its permissions,
+# etc. So although this class does not explicitly document all instance methods
+# supported by File, you can in fact call any File instance method on a
+# Tempfile object.
+#
+# A Tempfile object has a finalizer to remove the temporary file.
+# This means that the temporary file is removed via GC.
+# This can cause several problems:
+#
+# - Long GC intervals and conservative GC can accumulate temporary files that are not removed.
+# - Temporary files are not removed if Ruby exits abnormally (such as SIGKILL, SEGV).
+#
+# There are legacy good practices for Tempfile.new and Tempfile.open as follows.
#
# === Explicit close
#
@@ -71,12 +134,17 @@ require 'tmpdir'
# be able to read from or write to the Tempfile, and you do not need to
# know the Tempfile's filename either.
#
+# Also, this guarantees the temporary file is removed even if Ruby exits abnormally.
+# The OS reclaims the storage for the temporary file when the file is closed or
+# the Ruby process exits (normally or abnormally).
+#
# For example, a practical use case for unlink-after-creation would be this:
# you need a large byte buffer that's too large to comfortably fit in RAM,
# e.g. when you're writing a web server and you want to buffer the client's
# file upload data.
#
-# Please refer to #unlink for more information and a code example.
+# `Tempfile.create(anonymous: true)` supports this behavior.
+# It also works on Windows.
#
# == Minor notes
#
@@ -153,45 +221,47 @@ class Tempfile < DelegateClass(File)
@unlinked = false
@mode = mode|File::RDWR|File::CREAT|File::EXCL
- @finalizer_obj = Object.new
tmpfile = nil
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
opts[:perm] = 0600
tmpfile = File.open(tmpname, @mode, **opts)
@opts = opts.freeze
end
- ObjectSpace.define_finalizer(@finalizer_obj, Remover.new(tmpfile.path))
- ObjectSpace.define_finalizer(self, Closer.new(tmpfile))
super(tmpfile)
+
+ @finalizer_manager = FinalizerManager.new(__getobj__.path)
+ @finalizer_manager.register(self, __getobj__)
end
def initialize_dup(other) # :nodoc:
initialize_copy_iv(other)
super(other)
- ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
+ @finalizer_manager.register(self, __getobj__)
end
def initialize_clone(other) # :nodoc:
initialize_copy_iv(other)
super(other)
- ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
+ @finalizer_manager.register(self, __getobj__)
end
private def initialize_copy_iv(other) # :nodoc:
@unlinked = other.unlinked
@mode = other.mode
@opts = other.opts
- @finalizer_obj = other.finalizer_obj
+ @finalizer_manager = other.finalizer_manager
end
# Opens or reopens the file with mode "r+".
def open
_close
- ObjectSpace.undefine_finalizer(self)
+
mode = @mode & ~(File::CREAT|File::EXCL)
__setobj__(File.open(__getobj__.path, mode, **@opts))
- ObjectSpace.define_finalizer(self, Closer.new(__getobj__))
+
+ @finalizer_manager.register(self, __getobj__)
+
__getobj__
end
@@ -259,7 +329,9 @@ class Tempfile < DelegateClass(File)
# may not be able to unlink on Windows; just ignore
return
end
- ObjectSpace.undefine_finalizer(@finalizer_obj)
+
+ @finalizer_manager.unlinked = true
+
@unlinked = true
end
alias delete unlink
@@ -293,35 +365,35 @@ class Tempfile < DelegateClass(File)
protected
- attr_reader :unlinked, :mode, :opts, :finalizer_obj
+ attr_reader :unlinked, :mode, :opts, :finalizer_manager
- class Closer # :nodoc:
- def initialize(tmpfile)
- @tmpfile = tmpfile
- end
-
- def call(*args)
- @tmpfile.close
- end
- end
+ class FinalizerManager # :nodoc:
+ attr_accessor :unlinked
- class Remover # :nodoc:
def initialize(path)
- @pid = Process.pid
+ @open_files = {}
@path = path
+ @pid = Process.pid
+ @unlinked = false
end
- def call(*args)
- return if @pid != Process.pid
+ def register(obj, file)
+ ObjectSpace.undefine_finalizer(obj)
+ ObjectSpace.define_finalizer(obj, self)
+ @open_files[obj.object_id] = file
+ end
- $stderr.puts "removing #{@path}..." if $DEBUG
+ def call(object_id)
+ @open_files.delete(object_id).close
- begin
- File.unlink(@path)
- rescue Errno::ENOENT
+ if @open_files.empty? && !@unlinked && Process.pid == @pid
+ $stderr.puts "removing #{@path}..." if $DEBUG
+ begin
+ File.unlink(@path)
+ rescue Errno::ENOENT
+ end
+ $stderr.puts "done" if $DEBUG
end
-
- $stderr.puts "done" if $DEBUG
end
end
@@ -392,8 +464,9 @@ end
# see {File Permissions}[rdoc-ref:File@File+Permissions].
# - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
#
-# With no block, the file is not removed automatically,
-# and so should be explicitly removed.
+# The temporary file removal depends on the keyword argument +anonymous+ and
+# whether a block is given or not.
+# See the description about the +anonymous+ keyword argument later.
#
# Example:
#
@@ -401,11 +474,36 @@ end
# f.class # => File
# f.path # => "/tmp/20220505-9795-17ky6f6"
# f.stat.mode.to_s(8) # => "100600"
+# f.close
# File.exist?(f.path) # => true
# File.unlink(f.path)
# File.exist?(f.path) # => false
#
-# Argument +basename+, if given, may be one of:
+# Tempfile.create {|f|
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.path # => "/tmp/20240524-380207-oma0ny"
+# File.exist?(f.path) # => true
+# } # The file is removed at block exit.
+#
+# f = Tempfile.create(anonymous: true)
+# # The file is already removed because anonymous
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# f.close
+#
+# Tempfile.create(anonymous: true) {|f|
+# # The file is already removed because anonymous
+# f.path # => "/tmp/" (no filename since no file)
+# f.puts "foo"
+# f.rewind
+# f.read # => "foo\n"
+# }
+#
+# The argument +basename+, if given, may be one of the following:
#
# - A string: the generated filename begins with +basename+:
#
@@ -416,27 +514,57 @@ end
#
# Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg>
#
-# With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
+# With arguments +basename+ and +tmpdir+, the file is created in the directory +tmpdir+:
#
# Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8>
#
-# Keyword arguments +mode+ and +options+ are passed directly to method
+# Keyword arguments +mode+ and +options+ are passed directly to the method
# {File.open}[rdoc-ref:File.open]:
#
-# - The value given with +mode+ must be an integer,
+# - The value given for +mode+ must be an integer
# and may be expressed as the logical OR of constants defined in
# {File::Constants}[rdoc-ref:File::Constants].
# - For +options+, see {Open Options}[rdoc-ref:IO@Open+Options].
#
-# With a block given, creates the file as above, passes it to the block,
-# and returns the block's value;
-# before the return, the file object is closed and the underlying file is removed:
+# The keyword argument +anonymous+ specifies when the file is removed.
+#
+# - <tt>anonymous=false</tt> (default) without a block: the file is not removed.
+# - <tt>anonymous=false</tt> (default) with a block: the file is removed after the block exits.
+# - <tt>anonymous=true</tt> without a block: the file is removed before returning.
+# - <tt>anonymous=true</tt> with a block: the file is removed before the block is called.
+#
+# In the first case (<tt>anonymous=false</tt> without a block),
+# the file is not removed automatically.
+# It should be explicitly closed.
+# It can be used to rename to the desired filename.
+# If the file is not needed, it should be explicitly removed.
+#
+# The File#path method of the created file object returns the temporary directory with a trailing slash
+# when +anonymous+ is true.
+#
+# When a block is given, it creates the file as described above, passes it to the block,
+# and returns the block's value.
+# Before the returning, the file object is closed and the underlying file is removed:
#
# Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists"
#
+# Implementation note:
+#
+# The keyword argument +anonymous=true+ is implemented using FILE_SHARE_DELETE on Windows.
+# O_TMPFILE is used on Linux.
+#
# Related: Tempfile.new.
#
-def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
+def Tempfile.create(basename="", tmpdir=nil, mode: 0, anonymous: false, **options, &block)
+ if anonymous
+ create_anonymous(basename, tmpdir, mode: mode, **options, &block)
+ else
+ create_with_filename(basename, tmpdir, mode: mode, **options, &block)
+ end
+end
+
+class << Tempfile
+private def create_with_filename(basename="", tmpdir=nil, mode: 0, **options)
tmpfile = nil
Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
mode |= File::RDWR|File::CREAT|File::EXCL
@@ -464,3 +592,51 @@ def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
tmpfile
end
end
+
+File.open(IO::NULL) do |f|
+ File.new(f.fileno, autoclose: false, path: "").path
+rescue IOError
+ module PathAttr # :nodoc:
+ attr_reader :path
+
+ def self.set_path(file, path)
+ file.extend(self).instance_variable_set(:@path, path)
+ end
+ end
+end
+
+private def create_anonymous(basename="", tmpdir=nil, mode: 0, **options, &block)
+ tmpfile = nil
+ tmpdir = Dir.tmpdir() if tmpdir.nil?
+ if defined?(File::TMPFILE) # O_TMPFILE since Linux 3.11
+ begin
+ tmpfile = File.open(tmpdir, File::RDWR | File::TMPFILE, 0600)
+ rescue Errno::EISDIR, Errno::ENOENT, Errno::EOPNOTSUPP
+ # kernel or the filesystem does not support O_TMPFILE
+ # fallback to create-and-unlink
+ end
+ end
+ if tmpfile.nil?
+ mode |= File::SHARE_DELETE | File::BINARY # Windows needs them to unlink the opened file.
+ tmpfile = create_with_filename(basename, tmpdir, mode: mode, **options)
+ File.unlink(tmpfile.path)
+ tmppath = tmpfile.path
+ end
+ path = File.join(tmpdir, '')
+ unless tmppath == path
+ # clear path.
+ tmpfile.autoclose = false
+ tmpfile = File.new(tmpfile.fileno, mode: File::RDWR, path: path)
+ PathAttr.set_path(tmpfile, path) if defined?(PathAttr)
+ end
+ if block
+ begin
+ yield tmpfile
+ ensure
+ tmpfile.close
+ end
+ else
+ tmpfile
+ end
+end
+end
diff --git a/lib/time.rb b/lib/time.rb
index b565130b50..10f75b1dda 100644
--- a/lib/time.rb
+++ b/lib/time.rb
@@ -26,7 +26,7 @@ require 'date'
class Time
- VERSION = "0.3.0"
+ VERSION = "0.4.0"
class << Time
@@ -695,35 +695,36 @@ class Time
getutc.strftime('%a, %d %b %Y %T GMT')
end
- #
- # Returns a string which represents the time as a dateTime defined by XML
- # Schema:
- #
- # CCYY-MM-DDThh:mm:ssTZD
- # CCYY-MM-DDThh:mm:ss.sssTZD
- #
- # where TZD is Z or [+-]hh:mm.
- #
- # If self is a UTC time, Z is used as TZD. [+-]hh:mm is used otherwise.
- #
- # +fraction_digits+ specifies a number of digits to use for fractional
- # seconds. Its default value is 0.
- #
- # require 'time'
- #
- # t = Time.now
- # t.iso8601 # => "2011-10-05T22:26:12-04:00"
- #
- # You must require 'time' to use this method.
- #
- def xmlschema(fraction_digits=0)
- fraction_digits = fraction_digits.to_i
- s = strftime("%FT%T")
- if fraction_digits > 0
- s << strftime(".%#{fraction_digits}N")
+ unless method_defined?(:xmlschema)
+ #
+ # Returns a string which represents the time as a dateTime defined by XML
+ # Schema:
+ #
+ # CCYY-MM-DDThh:mm:ssTZD
+ # CCYY-MM-DDThh:mm:ss.sssTZD
+ #
+ # where TZD is Z or [+-]hh:mm.
+ #
+ # If self is a UTC time, Z is used as TZD. [+-]hh:mm is used otherwise.
+ #
+ # +fraction_digits+ specifies a number of digits to use for fractional
+ # seconds. Its default value is 0.
+ #
+ # require 'time'
+ #
+ # t = Time.now
+ # t.iso8601 # => "2011-10-05T22:26:12-04:00"
+ #
+ # You must require 'time' to use this method.
+ #
+ def xmlschema(fraction_digits=0)
+ fraction_digits = fraction_digits.to_i
+ s = strftime("%FT%T")
+ if fraction_digits > 0
+ s << strftime(".%#{fraction_digits}N")
+ end
+ s << (utc? ? 'Z' : strftime("%:z"))
end
- s << (utc? ? 'Z' : strftime("%:z"))
end
- alias iso8601 xmlschema
+ alias iso8601 xmlschema unless method_defined?(:iso8601)
end
-
diff --git a/lib/timeout.rb b/lib/timeout.rb
index c67a748856..6448f4f114 100644
--- a/lib/timeout.rb
+++ b/lib/timeout.rb
@@ -4,7 +4,7 @@
# == Synopsis
#
# require 'timeout'
-# status = Timeout::timeout(5) {
+# status = Timeout.timeout(5) {
# # Something that should be interrupted if it takes more than 5 seconds...
# }
#
@@ -13,10 +13,6 @@
# Timeout provides a way to auto-terminate a potentially long-running
# operation if it hasn't finished in a fixed amount of time.
#
-# Previous versions didn't use a module for namespacing, however
-# #timeout is provided for backwards compatibility. You
-# should prefer Timeout.timeout instead.
-#
# == Copyright
#
# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
diff --git a/lib/tmpdir.rb b/lib/tmpdir.rb
index fe3e0e19d1..66ac7cfb32 100644
--- a/lib/tmpdir.rb
+++ b/lib/tmpdir.rb
@@ -148,7 +148,11 @@ class Dir
# Generates and yields random names to create a temporary name
def create(basename, tmpdir=nil, max_try: nil, **opts)
origdir = tmpdir
- tmpdir ||= tmpdir()
+ if tmpdir
+ raise ArgumentError, "empty parent path" if tmpdir.empty?
+ else
+ tmpdir = tmpdir()
+ end
n = nil
prefix, suffix = basename
prefix = (String.try_convert(prefix) or
diff --git a/lib/uri/common.rb b/lib/uri/common.rb
index dce09fbc1e..904df10663 100644
--- a/lib/uri/common.rb
+++ b/lib/uri/common.rb
@@ -13,24 +13,46 @@ require_relative "rfc2396_parser"
require_relative "rfc3986_parser"
module URI
- include RFC2396_REGEXP
+ RFC2396_PARSER = RFC2396_Parser.new
+ Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor)
- REGEXP = RFC2396_REGEXP
- Parser = RFC2396_Parser
RFC3986_PARSER = RFC3986_Parser.new
Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor)
- # URI::Parser.new
- DEFAULT_PARSER = Parser.new
- DEFAULT_PARSER.pattern.each_pair do |sym, str|
- unless REGEXP::PATTERN.const_defined?(sym)
- REGEXP::PATTERN.const_set(sym, str)
+ DEFAULT_PARSER = RFC3986_PARSER
+ Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor)
+
+ def self.parser=(parser = RFC3986_PARSER)
+ remove_const(:Parser) if defined?(::URI::Parser)
+ const_set("Parser", parser.class)
+
+ remove_const(:REGEXP) if defined?(::URI::REGEXP)
+ remove_const(:PATTERN) if defined?(::URI::PATTERN)
+ if Parser == RFC2396_Parser
+ const_set("REGEXP", URI::RFC2396_REGEXP)
+ const_set("PATTERN", URI::RFC2396_REGEXP::PATTERN)
+ Parser.new.pattern.each_pair do |sym, str|
+ unless REGEXP::PATTERN.const_defined?(sym)
+ REGEXP::PATTERN.const_set(sym, str)
+ end
+ end
+ end
+
+ Parser.new.regexp.each_pair do |sym, str|
+ remove_const(sym) if const_defined?(sym)
+ const_set(sym, str)
end
end
- DEFAULT_PARSER.regexp.each_pair do |sym, str|
- const_set(sym, str)
+ self.parser = RFC3986_PARSER
+
+ def self.const_missing(const)
+ if value = RFC2396_PARSER.regexp[const]
+ warn "URI::#{const} is obsolete. Use RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE
+ value
+ else
+ super
+ end
end
- Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor)
module Util # :nodoc:
def make_components_hash(klass, array_hash)
@@ -168,7 +190,7 @@ module URI
# ["fragment", "top"]]
#
def self.split(uri)
- RFC3986_PARSER.split(uri)
+ DEFAULT_PARSER.split(uri)
end
# Returns a new \URI object constructed from the given string +uri+:
@@ -182,7 +204,7 @@ module URI
# if it may contain invalid URI characters.
#
def self.parse(uri)
- RFC3986_PARSER.parse(uri)
+ DEFAULT_PARSER.parse(uri)
end
# Merges the given URI strings +str+
@@ -209,7 +231,7 @@ module URI
# # => #<URI::HTTP http://example.com/foo/bar>
#
def self.join(*str)
- RFC3986_PARSER.join(*str)
+ DEFAULT_PARSER.join(*str)
end
#
diff --git a/lib/uri/file.rb b/lib/uri/file.rb
index 4ff0bc097e..940d361af8 100644
--- a/lib/uri/file.rb
+++ b/lib/uri/file.rb
@@ -70,17 +70,17 @@ module URI
# raise InvalidURIError
def check_userinfo(user)
- raise URI::InvalidURIError, "can not set userinfo for file URI"
+ raise URI::InvalidURIError, "cannot set userinfo for file URI"
end
# raise InvalidURIError
def check_user(user)
- raise URI::InvalidURIError, "can not set user for file URI"
+ raise URI::InvalidURIError, "cannot set user for file URI"
end
# raise InvalidURIError
def check_password(user)
- raise URI::InvalidURIError, "can not set password for file URI"
+ raise URI::InvalidURIError, "cannot set password for file URI"
end
# do nothing
diff --git a/lib/uri/generic.rb b/lib/uri/generic.rb
index bdd366661e..d4bfa3b919 100644
--- a/lib/uri/generic.rb
+++ b/lib/uri/generic.rb
@@ -82,7 +82,7 @@ module URI
if args.kind_of?(Array)
return self.build(args.collect{|x|
if x.is_a?(String)
- DEFAULT_PARSER.escape(x)
+ URI::RFC2396_PARSER.escape(x)
else
x
end
@@ -91,7 +91,7 @@ module URI
tmp = {}
args.each do |key, value|
tmp[key] = if value
- DEFAULT_PARSER.escape(value)
+ URI::RFC2396_PARSER.escape(value)
else
value
end
@@ -393,7 +393,7 @@ module URI
def check_user(v)
if @opaque
raise InvalidURIError,
- "can not set user with opaque"
+ "cannot set user with opaque"
end
return v unless v
@@ -417,7 +417,7 @@ module URI
def check_password(v, user = @user)
if @opaque
raise InvalidURIError,
- "can not set password with opaque"
+ "cannot set password with opaque"
end
return v unless v
@@ -596,7 +596,7 @@ module URI
if @opaque
raise InvalidURIError,
- "can not set host with registry or opaque"
+ "cannot set host with registry or opaque"
elsif parser.regexp[:HOST] !~ v
raise InvalidComponentError,
"bad component(expected host component): #{v}"
@@ -685,7 +685,7 @@ module URI
if @opaque
raise InvalidURIError,
- "can not set port with registry or opaque"
+ "cannot set port with registry or opaque"
elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
raise InvalidComponentError,
"bad component(expected port component): #{v.inspect}"
@@ -733,17 +733,17 @@ module URI
end
def check_registry(v) # :nodoc:
- raise InvalidURIError, "can not set registry"
+ raise InvalidURIError, "cannot set registry"
end
private :check_registry
def set_registry(v) #:nodoc:
- raise InvalidURIError, "can not set registry"
+ raise InvalidURIError, "cannot set registry"
end
protected :set_registry
def registry=(v)
- raise InvalidURIError, "can not set registry"
+ raise InvalidURIError, "cannot set registry"
end
#
@@ -866,7 +866,7 @@ module URI
# hier_part = ( net_path | abs_path ) [ "?" query ]
if @host || @port || @user || @path # userinfo = @user + ':' + @password
raise InvalidURIError,
- "can not set opaque with host, port, userinfo or path"
+ "cannot set opaque with host, port, userinfo or path"
elsif v && parser.regexp[:OPAQUE] !~ v
raise InvalidComponentError,
"bad component(expected opaque component): #{v}"
@@ -1235,7 +1235,7 @@ module URI
return rel, rel
end
- # you can modify `rel', but can not `oth'.
+ # you can modify `rel', but cannot `oth'.
return oth, rel
end
private :route_from0
@@ -1260,7 +1260,7 @@ module URI
# #=> #<URI::Generic /main.rbx?page=1>
#
def route_from(oth)
- # you can modify `rel', but can not `oth'.
+ # you can modify `rel', but cannot `oth'.
begin
oth, rel = route_from0(oth)
rescue
diff --git a/lib/uri/rfc2396_parser.rb b/lib/uri/rfc2396_parser.rb
index 00c66cf042..a56ca34267 100644
--- a/lib/uri/rfc2396_parser.rb
+++ b/lib/uri/rfc2396_parser.rb
@@ -140,11 +140,11 @@ module URI
if !scheme
raise InvalidURIError,
- "bad URI(absolute but no scheme): #{uri}"
+ "bad URI (absolute but no scheme): #{uri}"
end
if !opaque && (!path && (!host && !registry))
raise InvalidURIError,
- "bad URI(absolute but no path): #{uri}"
+ "bad URI (absolute but no path): #{uri}"
end
when @regexp[:REL_URI]
@@ -173,7 +173,7 @@ module URI
# server = [ [ userinfo "@" ] hostport ]
else
- raise InvalidURIError, "bad URI(is not URI?): #{uri}"
+ raise InvalidURIError, "bad URI (is not URI?): #{uri}"
end
path = '' if !path && !opaque # (see RFC2396 Section 5.2)
diff --git a/lib/uri/rfc3986_parser.rb b/lib/uri/rfc3986_parser.rb
index 092a1ac89d..4000f1357f 100644
--- a/lib/uri/rfc3986_parser.rb
+++ b/lib/uri/rfc3986_parser.rb
@@ -78,7 +78,7 @@ module URI
begin
uri = uri.to_str
rescue NoMethodError
- raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
+ raise InvalidURIError, "bad URI (is not URI?): #{uri.inspect}"
end
uri.ascii_only? or
raise InvalidURIError, "URI must be ascii only #{uri.dump}"
@@ -127,7 +127,7 @@ module URI
m["fragment"]
]
else
- raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
+ raise InvalidURIError, "bad URI (is not URI?): #{uri.inspect}"
end
end
@@ -135,12 +135,35 @@ module URI
URI.for(*self.split(uri), self)
end
-
def join(*uris) # :nodoc:
uris[0] = convert_to_uri(uris[0])
uris.inject :merge
end
+ # Compatibility for RFC2396 parser
+ def extract(str, schemes = nil, &block) # :nodoc:
+ warn "URI::RFC3986_PARSER.extract is obsoleted. Use URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.extract(str, schemes, &block)
+ end
+
+ # Compatibility for RFC2396 parser
+ def make_regexp(schemes = nil) # :nodoc:
+ warn "URI::RFC3986_PARSER.make_regexp is obsoleted. Use URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.make_regexp(schemes)
+ end
+
+ # Compatibility for RFC2396 parser
+ def escape(str, unsafe = nil) # :nodoc:
+ warn "URI::RFC3986_PARSER.escape is obsoleted. Use URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE
+ unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str)
+ end
+
+ # Compatibility for RFC2396 parser
+ def unescape(str, escaped = nil) # :nodoc:
+ warn "URI::RFC3986_PARSER.unescape is obsoleted. Use URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE
+ escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str)
+ end
+
@@to_s = Kernel.instance_method(:to_s)
if @@to_s.respond_to?(:bind_call)
def inspect
diff --git a/lib/uri/version.rb b/lib/uri/version.rb
index 2dafa57d59..bfe3f47670 100644
--- a/lib/uri/version.rb
+++ b/lib/uri/version.rb
@@ -1,6 +1,6 @@
module URI
# :stopdoc:
- VERSION_CODE = '001300'.freeze
+ VERSION_CODE = '001301'.freeze
VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze
# :startdoc:
end