From da60c90766500b0e3c0927af1d2efbddec93d35a Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 11 Mar 2026 13:47:11 -0400 Subject: [PATCH 01/14] [ruby/prism] Improve pm_regexp_classify_property perf https://github.com/ruby/prism/commit/3bdd79257b --- prism/regexp.c | 84 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/prism/regexp.c b/prism/regexp.c index 93711d6b947f61..f864e187c9ca13 100644 --- a/prism/regexp.c +++ b/prism/regexp.c @@ -430,15 +430,6 @@ typedef enum { PM_REGEXP_PROPERTY_UNICODE } pm_regexp_property_type_t; -/** - * Check if a property name matches a NUL-terminated target string - * (case-insensitive, exact length match). - */ -static inline bool -pm_regexp_property_name_matches(const uint8_t *name, size_t length, const char *target) { - return strlen(target) == length && pm_strncasecmp(name, (const uint8_t *) target, length) == 0; -} - /** * Classify a property name. The name may start with '^' for negation, which * is skipped before matching. @@ -451,30 +442,63 @@ pm_regexp_classify_property(const uint8_t *name, size_t length) { length--; } - // POSIX properties — valid in all encodings. - static const char *const posix_properties[] = { - "Alnum", "Alpha", "ASCII", "Blank", "Cntrl", "Digit", "Graph", - "Lower", "Print", "Punct", "Space", "Upper", "XDigit", "Word", - NULL - }; +#define PM_REGEXP_CASECMP(str_) (pm_strncasecmp(name, (const uint8_t *) (str_), length) == 0) - for (const char *const *property = posix_properties; *property != NULL; property++) { - if (pm_regexp_property_name_matches(name, length, *property)) { - return PM_REGEXP_PROPERTY_POSIX; - } + switch (length) { + case 3: + if (PM_REGEXP_CASECMP("Han")) return PM_REGEXP_PROPERTY_SCRIPT; + break; + case 4: + if (PM_REGEXP_CASECMP("Word")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 5: + /* Most properties are length 5, so dispatch on first character. */ + switch (name[0] | 0x20) { + case 'a': + if (PM_REGEXP_CASECMP("Alnum")) return PM_REGEXP_PROPERTY_POSIX; + if (PM_REGEXP_CASECMP("Alpha")) return PM_REGEXP_PROPERTY_POSIX; + if (PM_REGEXP_CASECMP("ASCII")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 'b': + if (PM_REGEXP_CASECMP("Blank")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 'c': + if (PM_REGEXP_CASECMP("Cntrl")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 'd': + if (PM_REGEXP_CASECMP("Digit")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 'g': + if (PM_REGEXP_CASECMP("Graph")) return PM_REGEXP_PROPERTY_POSIX; + if (PM_REGEXP_CASECMP("Greek")) return PM_REGEXP_PROPERTY_SCRIPT; + break; + case 'l': + if (PM_REGEXP_CASECMP("Lower")) return PM_REGEXP_PROPERTY_POSIX; + if (PM_REGEXP_CASECMP("Latin")) return PM_REGEXP_PROPERTY_SCRIPT; + break; + case 'p': + if (PM_REGEXP_CASECMP("Print")) return PM_REGEXP_PROPERTY_POSIX; + if (PM_REGEXP_CASECMP("Punct")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 's': + if (PM_REGEXP_CASECMP("Space")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 'u': + if (PM_REGEXP_CASECMP("Upper")) return PM_REGEXP_PROPERTY_POSIX; + break; + } + break; + case 6: + if (PM_REGEXP_CASECMP("XDigit")) return PM_REGEXP_PROPERTY_POSIX; + break; + case 8: + if (PM_REGEXP_CASECMP("Hiragana")) return PM_REGEXP_PROPERTY_SCRIPT; + if (PM_REGEXP_CASECMP("Katakana")) return PM_REGEXP_PROPERTY_SCRIPT; + if (PM_REGEXP_CASECMP("Cyrillic")) return PM_REGEXP_PROPERTY_SCRIPT; + break; } - // Script properties — valid in /e, /s, /u but not /n. - static const char *const script_properties[] = { - "Hiragana", "Katakana", "Han", "Latin", "Greek", "Cyrillic", - NULL - }; - - for (const char *const *property = script_properties; *property != NULL; property++) { - if (pm_regexp_property_name_matches(name, length, *property)) { - return PM_REGEXP_PROPERTY_SCRIPT; - } - } +#undef PM_REGEXP_CASECMP // Everything else is Unicode-only (general categories, other scripts, etc.). return PM_REGEXP_PROPERTY_UNICODE; From 85e434a86036b0a6dd596d49af2f948da78ff977 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 11 Mar 2026 12:40:21 -0400 Subject: [PATCH 02/14] [ruby/prism] Track newlines in character escape sequences https://github.com/ruby/prism/commit/2e58c52196 --- prism/prism.c | 5 +++++ test/prism/newline_offsets_test.rb | 27 +++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index d196c5d7c4c919..9d58bdb43d2eb4 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -8594,6 +8594,7 @@ escape_write_escape_encoded(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_ } if (width == 1) { + if (*parser->current.end == '\n') pm_line_offset_list_append(&parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(*parser->current.end++, flags)); } else if (width > 1) { // Valid multibyte character. Just ignore escape. @@ -8910,6 +8911,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } + if (peeked == '\n') pm_line_offset_list_append(&parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_CONTROL)); return; @@ -8968,6 +8970,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } + if (peeked == '\n') pm_line_offset_list_append(&parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_CONTROL)); return; @@ -9021,6 +9024,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre return; } + if (peeked == '\n') pm_line_offset_list_append(&parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 1); parser->current.end++; escape_write_byte(parser, buffer, regular_expression_buffer, flags, escape_byte(peeked, flags | PM_ESCAPE_FLAG_META)); return; @@ -9028,6 +9032,7 @@ escape_read(pm_parser_t *parser, pm_buffer_t *buffer, pm_buffer_t *regular_expre } case '\r': { if (peek_offset(parser, 1) == '\n') { + pm_line_offset_list_append(&parser->line_offsets, PM_TOKEN_END(parser, &parser->current) + 2); parser->current.end += 2; escape_write_byte_encoded(parser, buffer, flags, escape_byte('\n', flags)); return; diff --git a/test/prism/newline_offsets_test.rb b/test/prism/newline_offsets_test.rb index 99b808b1df6571..bb06876a967076 100644 --- a/test/prism/newline_offsets_test.rb +++ b/test/prism/newline_offsets_test.rb @@ -8,15 +8,38 @@ class NewlineOffsetsTest < TestCase define_method(fixture.test_name) { assert_newline_offsets(fixture) } end + def test_escape_control_newline + # Newlines consumed inside escape sequences like \C-, \c, and \M- + # must be tracked in line offsets across all literal types. + %w[\\C- \\c \\M-].each do |escape| + assert_newline_offsets_for("\"#{escape}\n\"", "#{escape} in string") + assert_newline_offsets_for("`#{escape}\n`", "#{escape} in xstring") + assert_newline_offsets_for("/#{escape}\n/", "#{escape} in regexp") + assert_newline_offsets_for("%Q{#{escape}\n}", "#{escape} in %Q") + assert_newline_offsets_for("%W[#{escape}\n]", "#{escape} in %W") + assert_newline_offsets_for("<<~H\n#{escape}\n\nH\n", "#{escape} in heredoc") + assert_newline_offsets_for("?#{escape}\n", "#{escape} in char literal") + end + + # Combined meta + control escapes + assert_newline_offsets_for("\"\\M-\\C-\n\"", "\\M-\\C- in string") + assert_newline_offsets_for("\"\\M-\\c\n\"", "\\M-\\c in string") + + # \r\n consumed inside escape context + assert_newline_offsets_for("\"\\C-\r\n\"", "\\C- with \\r\\n") + end + private def assert_newline_offsets(fixture) - source = fixture.read + assert_newline_offsets_for(fixture.read) + end + def assert_newline_offsets_for(source, message = nil) expected = [0] source.b.scan("\n") { expected << $~.offset(0)[0] + 1 } - assert_equal expected, Prism.parse(source).source.offsets + assert_equal expected, Prism.parse(source).source.offsets, message end end end From 943c9cadf211a5def508a77dfdbc124d62121fdf Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Wed, 11 Mar 2026 18:36:44 -0500 Subject: [PATCH 03/14] [DOC] Doc for Pathname.mktmpdir (#16365) --- lib/pathname.rb | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index b19e379cd48d9b..78c440416ea060 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -55,11 +55,35 @@ def rmtree(noop: nil, verbose: nil, secure: nil) end class Pathname # * tmpdir * - # Creates a tmp directory and wraps the returned path in a Pathname object. + # call-seq: + # Pathname.mktmpdir -> new_pathname + # Pathname.mktmpdir {|pathname| ... } -> object # - # Note that you need to require 'pathname' to use this method. + # Creates: + # + # - A temporary directory via Dir.mktmpdir. + # - A \Pathname object that contains the path to that directory. + # + # With no block given, returns the created pathname; + # the caller should delete the created directory when it is no longer needed + # (FileUtils.rm_r is a convenient method for the deletion): + # + # pathname = Pathname.mktmpdir + # dirpath = pathname.to_s + # Dir.exist?(dirpath) # => true + # # Do something with the directory. + # require 'fileutils' + # FileUtils.rm_r(dirpath) + # + # With a block given, calls the block with the created pathname; + # on block exit, automatically deletes the created directory and all its contents; + # returns the block's exit value: # - # See Dir.mktmpdir + # pathname = Pathname.mktmpdir do |p| + # # Do something with the directory. + # p + # end + # Dir.exist?(pathname.to_s) # => false def self.mktmpdir require 'tmpdir' unless defined?(Dir.mktmpdir) if block_given? From aa604d5a81bb782cc9f26bca24779b3478cec682 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 11 Mar 2026 19:45:09 -0400 Subject: [PATCH 04/14] [DOC] Fix indentation in docs for File.path --- file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index 193dbea10dd7f1..34c23e04e3a40e 100644 --- a/file.c +++ b/file.c @@ -5269,10 +5269,10 @@ rb_file_s_extname(VALUE klass, VALUE fname) } /* - * call-seq: + * call-seq: * File.path(path) -> string * - * Returns the string representation of the path + * Returns the string representation of the path * * File.path(File::NULL) #=> "/dev/null" * File.path(Pathname.new("/tmp")) #=> "/tmp" From f30833082f3933eb89a905163e09cdd6c7efcd44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 02:10:34 +0000 Subject: [PATCH 05/14] Bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby) and [advanced-security/filter-sarif](https://github.com/advanced-security/filter-sarif). Updates `ruby/setup-ruby` from 1.290.0 to 1.292.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c...4eb9f110bac952a8b68ecf92e3b5c7a987594ba6) Updates `advanced-security/filter-sarif` from 1.0.1 to 1.1 - [Release notes](https://github.com/advanced-security/filter-sarif/releases) - [Commits](https://github.com/advanced-security/filter-sarif/compare/f3b8118a9349d88f7b1c0c488476411145b6270d...2da736ff05ef065cb2894ac6892e47b5eac2c3c0) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.292.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: advanced-security/filter-sarif dependency-version: '1.1' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/annocheck.yml | 2 +- .github/workflows/auto_review_pr.yml | 2 +- .github/workflows/baseruby.yml | 2 +- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/check_dependencies.yml | 2 +- .github/workflows/check_misc.yml | 2 +- .github/workflows/check_sast.yml | 2 +- .github/workflows/modgc.yml | 2 +- .github/workflows/parse_y.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/spec_guards.yml | 2 +- .github/workflows/sync_default_gems.yml | 2 +- .github/workflows/ubuntu.yml | 2 +- .github/workflows/wasm.yml | 2 +- .github/workflows/windows.yml | 2 +- .github/workflows/yjit-ubuntu.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index 769d06db204527..8f538825d6938f 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -73,7 +73,7 @@ jobs: builddir: build makeup: true - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index 9cb8cf0f2e1ad4..258cbedfb3df76 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 9ac48df2406118..87d222a1893d43 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -48,7 +48,7 @@ jobs: - ruby-3.3 steps: - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index 4a0dce7847673d..03205013213305 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: 4.0 diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 63ee1909bbc06c..43c4be167a7b7f 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -42,7 +42,7 @@ jobs: - uses: ./.github/actions/setup/directories - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index a91b6a6f6e89ac..0e74a9affd441a 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,7 +23,7 @@ jobs: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: head diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 7192f878810607..bc044a9dbb2f4f 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -112,7 +112,7 @@ jobs: output: sarif-results - name: filter-sarif - uses: advanced-security/filter-sarif@f3b8118a9349d88f7b1c0c488476411145b6270d # v1.0.1 + uses: advanced-security/filter-sarif@2da736ff05ef065cb2894ac6892e47b5eac2c3c0 # v1.1.0.1.1 with: patterns: | +**/*.rb diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index 997e3e8617b7ec..5d930c89438103 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -62,7 +62,7 @@ jobs: uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index 77d7a8ea07208a..0c831e8f6e0658 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -59,7 +59,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71eca66d83efe1..a2bbd70ce413c0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: 3.3.4 diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index f944e92a2ae199..d2d788df699c38 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index f09c8ce88f153f..3f38db88c93827 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -36,7 +36,7 @@ jobs: with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index ac9f8187efa771..77792ff00d4ad9 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -70,7 +70,7 @@ jobs: with: arch: ${{ matrix.arch }} - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index a25348a56407c5..9eb7976a613832 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -99,7 +99,7 @@ jobs: run: | echo "WASI_SDK_PATH=/opt/wasi-sdk" >> $GITHUB_ENV - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 302c6e81cf638c..9b0619421f8632 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -59,7 +59,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 4e2ccd980831e4..6cbdc12fb21028 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -133,7 +133,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 20ef3a62654371..acfb4a217c6099 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -114,7 +114,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@6ca151fd1bfcfd6fe0c4eb6837eb0584d0134a0c # v1.290.0 + - uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1.292.0 with: ruby-version: '3.1' bundler: none From 93a516dd8bb0e9bd4e5654d44fb83d7ce81a10d6 Mon Sep 17 00:00:00 2001 From: lolwut Date: Thu, 12 Mar 2026 10:21:19 +0900 Subject: [PATCH 06/14] [ruby/rubygems] Revert DEFAULT_INSTALL_EXTENSION_IN_LIB to true Many gems created with the default template over the past 6 years use `require_relative` to load compiled extensions, which breaks when extensions are not copied into the gem's lib directory. Restore the default to true for now to maintain compatibility, and plan to change it to false in RubyGems 4.2 with advance warning. https://github.com/ruby/rubygems/commit/5e83a62a8e Co-Authored-By: Claude Opus 4.6 --- lib/rubygems/config_file.rb | 2 +- test/rubygems/test_gem_config_file.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index bd66aa258dad22..19718829fccbdd 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -47,7 +47,7 @@ class Gem::ConfigFile DEFAULT_CONCURRENT_DOWNLOADS = 8 DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365 DEFAULT_IPV4_FALLBACK_ENABLED = false - DEFAULT_INSTALL_EXTENSION_IN_LIB = false + DEFAULT_INSTALL_EXTENSION_IN_LIB = true DEFAULT_GLOBAL_GEM_CACHE = false DEFAULT_USE_PSYCH = false diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index b38628fdc4d85c..e85d00530e3089 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -43,7 +43,7 @@ def test_initialize assert_equal [@gem_repo], Gem.sources assert_equal 365, @cfg.cert_expiration_length_days assert_equal false, @cfg.ipv4_fallback_enabled - assert_equal false, @cfg.install_extension_in_lib + assert_equal true, @cfg.install_extension_in_lib File.open @temp_conf, "w" do |fp| fp.puts ":backtrace: true" @@ -59,7 +59,7 @@ def test_initialize fp.puts ":ssl_verify_mode: 0" fp.puts ":ssl_ca_cert: /etc/ssl/certs" fp.puts ":cert_expiration_length_days: 28" - fp.puts ":install_extension_in_lib: true" + fp.puts ":install_extension_in_lib: false" fp.puts ":ipv4_fallback_enabled: true" end @@ -75,7 +75,7 @@ def test_initialize assert_equal 0, @cfg.ssl_verify_mode assert_equal "/etc/ssl/certs", @cfg.ssl_ca_cert assert_equal 28, @cfg.cert_expiration_length_days - assert_equal true, @cfg.install_extension_in_lib + assert_equal false, @cfg.install_extension_in_lib assert_equal true, @cfg.ipv4_fallback_enabled end From aab7a55cd362540e7d67a2cf12c2f270d0d6bb59 Mon Sep 17 00:00:00 2001 From: lolwut Date: Thu, 12 Mar 2026 10:24:30 +0900 Subject: [PATCH 07/14] [ruby/rubygems] Warn when require_relative is used to load compiled extensions during gem build When a gem has native extensions and uses `require_relative` to load a path without a corresponding .rb file in the gem, warn that this will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory. Recommend using `require` instead. https://github.com/ruby/rubygems/commit/1198c24a08 Co-Authored-By: Claude Opus 4.6 --- lib/rubygems/specification_policy.rb | 28 +++++++++++++ test/rubygems/test_gem_specification.rb | 55 +++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 0fcb635394c433..8ab82ee4a9a1b9 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -476,6 +476,7 @@ def validate_extensions # :nodoc: validate_rake_extensions(builder) validate_rust_extensions(builder) + validate_extension_require_relative end def validate_rust_extensions(builder) # :nodoc: @@ -496,6 +497,33 @@ def validate_rake_extensions(builder) # :nodoc: WARNING end + def validate_extension_require_relative # :nodoc: + return unless @specification.extensions.any? + + require_paths = @specification.require_paths + + @specification.files.each do |rb_file| + next unless rb_file.end_with?(".rb") + next unless require_paths.any? {|rp| rb_file.start_with?("#{rp}/") } + next unless File.file?(rb_file) + + File.foreach(rb_file).with_index(1) do |line, lineno| + next unless line =~ /^\s*require_relative\s+["']([^"']+)["']/ + + required_path = Regexp.last_match(1) + resolved = File.join(File.dirname(rb_file), required_path) + + next if @specification.files.any? {|f| f == "#{resolved}.rb" || f == resolved } + + warning <<~WARNING + #{rb_file}:#{lineno} uses `require_relative "#{required_path}"` to load a compiled extension. + This will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory. + Use `require` instead of `require_relative` to load compiled extensions. + WARNING + end + end + end + def validate_unique_links links = @specification.metadata.slice(*METADATA_LINK_KEYS) grouped = links.group_by {|_key, uri| uri } diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 7675ade415c608..cf01a40b8c6416 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -2887,6 +2887,61 @@ def test_validate_rust_extension_have_no_missing_cargo_toml_error end end + def test_validate_extension_require_relative_warning + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = ["ext/a/extconf.rb"] + @a1.files = %w[lib/code.rb lib/a.rb ext/a/extconf.rb] + + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + + use_ui @ui do + @a1.validate + end + + assert_match(%r{require_relative "a/a"}, @ui.error) + assert_match(/will break in RubyGems 4\.2/, @ui.error) + assert_match(/Use `require` instead of `require_relative`/, @ui.error) + end + end + + def test_validate_extension_require_relative_no_warning_when_rb_exists + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = ["ext/a/extconf.rb"] + @a1.files = %w[lib/code.rb lib/a.rb lib/a/a.rb ext/a/extconf.rb] + + FileUtils.mkdir_p File.join("lib", "a") + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + File.write File.join("lib", "a", "a.rb"), "" + + use_ui @ui do + @a1.validate + end + + refute_match(/require_relative/, @ui.error) + end + end + + def test_validate_extension_require_relative_no_warning_without_extensions + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = [] + @a1.files = %w[lib/code.rb lib/a.rb] + + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + + use_ui @ui do + @a1.validate + end + + refute_match(/require_relative/, @ui.error) + end + end + def test_validate_description util_setup_validate From 1c7b2d2b0073681af400c5d1576fe3f2ec055010 Mon Sep 17 00:00:00 2001 From: lolwut Date: Thu, 12 Mar 2026 09:28:31 +0900 Subject: [PATCH 08/14] [ruby/rubygems] Support YAML 1.1 !binary tag in YAMLSerializer Decode base64-encoded values tagged with !binary in mapping keys, mapping values (both inline and block scalar), and sequence items. This fixes gem install failures for gems packaged with older RubyGems that used !binary encoding in checksums.yaml.gz. Fixes https://github.com/ruby/rubygems/issues/9387 https://github.com/ruby/rubygems/commit/cfc31601e9 Co-Authored-By: Claude Opus 4.6 --- lib/rubygems/yaml_serializer.rb | 22 +++++++++++++ test/rubygems/test_gem_safe_yaml.rb | 51 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 1721519c153fea..da6e72f5936e45 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -132,6 +132,8 @@ def parse_sequence_item(content, indent) @lines.any? && current_indent > indent ? parse_node(indent) : nil elsif content.start_with?("!ruby/object:") parse_tagged_content(content.strip, indent) + elsif content.start_with?("!binary ") + parse_binary_value(content, indent) elsif content.start_with?("-") @lines.unshift("#{" " * (indent + 2)}#{content}") parse_node(indent) @@ -156,6 +158,8 @@ def parse_mapping(indent, anchor) @lines.shift val = strip_comment($2.to_s.strip) + key = decode_binary_tag(key) if key.start_with?("!binary ") + val_anchor, val = consume_value_anchor(val) value = parse_mapping_value(val, indent) value = register_anchor(val_anchor, value) if val_anchor @@ -170,6 +174,8 @@ def parse_mapping_value(val, indent) parse_inline_alias(val) elsif val.start_with?("!ruby/object:") parse_tagged_content(val.strip, indent) + elsif val.start_with?("!binary ") + parse_binary_value(val, indent) elsif val.empty? next_stripped = nil next_indent = nil @@ -306,6 +312,22 @@ def coerce(val) end end + def decode_binary_tag(str) + content = str.sub(/\A!binary\s+/, "") + content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/ + content.unpack1("m") + end + + def parse_binary_value(val, indent) + rest = val.sub(/\A!binary\s+/, "") + if rest.start_with?("|") + content = parse_block_scalar(indent, rest[1..].to_s.strip) + Scalar.new(value: content.unpack1("m")) + else + Scalar.new(value: decode_binary_tag(val)) + end + end + def parse_alias_ref AliasRef.new(name: @lines.shift.lstrip[1..].strip) end diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 47e6dd68774ca6..d9faff0e05f951 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1135,4 +1135,55 @@ def test_regression_requirements_field_normalized_to_array spec = Gem::SafeYAML.safe_load(yaml) assert_equal [["foo", "bar"]], spec.requirements end + + def test_binary_tag_decoded_in_mapping_key + yaml = <<~YAML + --- + !binary "U0hBMQ==": + metadata.gz: abc123 + YAML + + result = yaml_load(yaml) + assert_equal "SHA1", result.keys.first + assert_equal "abc123", result["SHA1"]["metadata.gz"] + end + + def test_binary_tag_decoded_in_block_scalar_value + yaml = <<~YAML + --- + SHA256: + metadata.gz: !binary |- + OWY4YTM5Y2MxOTc3Mzc5MWYzNzk1NjRmZjVlYzljYjY1MDQwYWIwMg== + YAML + + result = yaml_load(yaml) + assert_equal "9f8a39cc19773791f379564ff5ec9cb65040ab02", result["SHA256"]["metadata.gz"] + end + + def test_binary_tag_decoded_in_inline_value + yaml = <<~YAML + --- + key: !binary "U0hBMQ==" + YAML + + result = yaml_load(yaml) + assert_equal "SHA1", result["key"] + end + + def test_binary_tag_checksums_yaml_roundtrip + # Simulates the checksums.yaml.gz format from older gems + yaml = <<~YAML + --- + !binary "U0hBMQ==": + metadata.gz: !binary |- + OWY4YTM5Y2MxOTc3Mzc5MWYzNzk1NjRmZjVlYzljYjY1MDQwYWIwMg== + data.tar.gz: !binary |- + ZTRmZGRhNjc1MWM5NmIwYzRhODFkYjI0OTlkMjY3ZjQ2MWNkMGM1ZA== + YAML + + result = yaml_load(yaml) + assert_equal ["SHA1"], result.keys + assert_equal "9f8a39cc19773791f379564ff5ec9cb65040ab02", result["SHA1"]["metadata.gz"] + assert_equal "e4fdda6751c96b0c4a81db2499d267f461cd0c5d", result["SHA1"]["data.tar.gz"] + end end From 1424201b13e3850be8a1b6666d937b232ab2fa35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 02:59:52 +0000 Subject: [PATCH 09/14] [ruby/rubygems] Add test for !binary tag in sequence item inline https://github.com/ruby/rubygems/commit/ba4a4b2b26 Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com> --- test/rubygems/test_gem_safe_yaml.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index d9faff0e05f951..088e931e4788c4 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1186,4 +1186,14 @@ def test_binary_tag_checksums_yaml_roundtrip assert_equal "9f8a39cc19773791f379564ff5ec9cb65040ab02", result["SHA1"]["metadata.gz"] assert_equal "e4fdda6751c96b0c4a81db2499d267f461cd0c5d", result["SHA1"]["data.tar.gz"] end + + def test_binary_tag_decoded_in_sequence_item_inline + yaml = <<~YAML + --- + - !binary "U0hBMQ==" + YAML + + result = yaml_load(yaml) + assert_equal ["SHA1"], result + end end From 0c6972b9d2f7ab4c39f5c80fcccef522328a7f17 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 11 Mar 2026 18:01:25 -0700 Subject: [PATCH 10/14] [ruby/rubygems] Restore original SafeYAML.load under Psych This was changed to unsafe_load with the swap to YAMLSerializer. But this method did not previously do an unsafe load and we shouldn't provide that. https://github.com/ruby/rubygems/commit/37f71c1eac --- lib/rubygems/safe_yaml.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb index c59b4653586282..4f696879813967 100644 --- a/lib/rubygems/safe_yaml.rb +++ b/lib/rubygems/safe_yaml.rb @@ -50,11 +50,7 @@ def self.safe_load(input) def self.load(input) if Gem.use_psych? - if ::Psych.respond_to?(:unsafe_load) - ::Psych.unsafe_load(input) - else - ::Psych.load(input) - end + ::Psych.safe_load(input, permitted_classes: [::Symbol]) else Gem::YAMLSerializer.load( input, From 7db82681ce6030a9019594a6489283b00186045f Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 11 Mar 2026 18:14:40 -0700 Subject: [PATCH 11/14] [ruby/rubygems] Unpend owner_command test https://github.com/ruby/rubygems/commit/227df53bc5 --- test/rubygems/test_gem_commands_owner_command.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index 8cd40e0eedde14..08df696616b8db 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -57,10 +57,6 @@ def test_show_owners def test_show_owners_dont_load_objects Gem.load_yaml - # Gem::SafeYAML.load uses Psych.unsafe_load when Psych is enabled, - # which does not restrict classes. Only YAMLSerializer restricts object tags. - pend "Gem::SafeYAML.load uses Psych.unsafe_load which does not restrict classes" if Gem.use_psych? - response = < Date: Wed, 11 Mar 2026 18:14:56 -0700 Subject: [PATCH 12/14] [ruby/rubygems] Use safe_load from owner_command This had been the only user of Gem::SafeYAML.load for a long time. We might as well be consistent with all other uses and use safe_load. https://github.com/ruby/rubygems/commit/1b698779f5 --- lib/rubygems/commands/owner_command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 12bfe3a834b703..18e612bc1b64ff 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -75,7 +75,7 @@ def show_owners(name) end with_response response do |resp| - owners = Gem::SafeYAML.load clean_text(resp.body) + owners = Gem::SafeYAML.safe_load clean_text(resp.body) say "Owners for gem: #{name}" owners.each do |owner| From d66f8d49bba3f966820652b746e92531394412fe Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 11 Mar 2026 18:39:03 -0700 Subject: [PATCH 13/14] [ruby/rubygems] Update load_yaml test helper to use safe_load https://github.com/ruby/rubygems/commit/51544ebfd8 --- test/rubygems/helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 783818b6eb6f52..5f5f2e03b10143 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -735,10 +735,10 @@ def write_dummy_extconf(gem_name) end ## - # Load a YAML string, the psych 3 way + # Load a YAML string using the safe loader with gem-spec permitted classes. def load_yaml(yaml) - Gem::SafeYAML.load(yaml) + Gem::SafeYAML.safe_load(yaml) end ## From 5c81ba21daa80bd14082c3c32b689057c0fd2349 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 11 Mar 2026 19:55:44 -0700 Subject: [PATCH 14/14] [ruby/rubygems] Make SafeYAML.load an alias of safe_load Using Psych, load was actually more restrictive than safe_load. Using Gem::YAMLSerializer they were identical. We might as well use the same path for both methods. https://github.com/ruby/rubygems/commit/d8d927f889 --- lib/rubygems/safe_yaml.rb | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb index 4f696879813967..f4bba001365fea 100644 --- a/lib/rubygems/safe_yaml.rb +++ b/lib/rubygems/safe_yaml.rb @@ -48,17 +48,8 @@ def self.safe_load(input) end end - def self.load(input) - if Gem.use_psych? - ::Psych.safe_load(input, permitted_classes: [::Symbol]) - else - Gem::YAMLSerializer.load( - input, - permitted_classes: PERMITTED_CLASSES, - permitted_symbols: PERMITTED_SYMBOLS, - aliases: aliases_enabled? - ) - end + class << self + alias_method :load, :safe_load end end end