From e1cfb1104cde3f7ffd7fb07aca835e4d0b09a98a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:22:28 -0700 Subject: [PATCH 01/11] Convert REXML::Security cvar to ivar Previously this was using class variables, however this module isn't included anywhere, so we might as well just use plain ivars --- lib/rexml/security.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/rexml/security.rb b/lib/rexml/security.rb index e8e8c6b4..486be198 100644 --- a/lib/rexml/security.rb +++ b/lib/rexml/security.rb @@ -1,28 +1,28 @@ # frozen_string_literal: false module REXML module Security - @@entity_expansion_limit = 10_000 + @entity_expansion_limit = 10_000 # Set the entity expansion limit. By default the limit is set to 10000. def self.entity_expansion_limit=( val ) - @@entity_expansion_limit = val + @entity_expansion_limit = val end # Get the entity expansion limit. By default the limit is set to 10000. def self.entity_expansion_limit - @@entity_expansion_limit + @entity_expansion_limit end - @@entity_expansion_text_limit = 10_240 + @entity_expansion_text_limit = 10_240 # Set the entity expansion limit. By default the limit is set to 10240. def self.entity_expansion_text_limit=( val ) - @@entity_expansion_text_limit = val + @entity_expansion_text_limit = val end # Get the entity expansion limit. By default the limit is set to 10240. def self.entity_expansion_text_limit - @@entity_expansion_text_limit + @entity_expansion_text_limit end end end From 9dba61654681106b28ed6298da4ee40b995a09ee Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:26:12 -0700 Subject: [PATCH 02/11] Make source.rb frozen_string_literal: true --- lib/rexml/source.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 8b8ba0da..d11a4418 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -1,5 +1,5 @@ # coding: US-ASCII -# frozen_string_literal: false +# frozen_string_literal: true require "stringio" require "strscan" @@ -78,6 +78,7 @@ module Private PRE_DEFINED_TERM_PATTERNS[term] = term end end + PRE_DEFINED_TERM_PATTERNS.freeze end private_constant :Private @@ -227,9 +228,9 @@ def initialize(arg, block_size=500, encoding=nil) @pending_buffer = nil if encoding - super("", encoding) + super(+"", encoding) else - super(@source.read(3) || "") + super(@source.read(3) || +"") end if !@to_utf and @@ -380,7 +381,7 @@ def encoding_updated @source.set_encoding(@encoding, @encoding) end @line_break = encode(">") - @pending_buffer, @scanner.string = @scanner.rest, "" + @pending_buffer, @scanner.string = @scanner.rest, +"" @pending_buffer.force_encoding(@encoding) super end From 08bdb32709ca7f07ac32217d40d111d9f9ebd249 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:26:39 -0700 Subject: [PATCH 03/11] Freeze constants in test.rb --- lib/rexml/text.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/rexml/text.rb b/lib/rexml/text.rb index 2319cef8..193db27f 100644 --- a/lib/rexml/text.rb +++ b/lib/rexml/text.rb @@ -11,11 +11,11 @@ module REXML class Text < Child include Comparable # The order in which the substitutions occur - SPECIALS = [ /&(?!#?[\w-]+;)/u, //u, /"/u, /'/u, /\r/u ] - SUBSTITUTES = ['&', '<', '>', '"', ''', ' '] + SPECIALS = [ /&(?!#?[\w-]+;)/u, //u, /"/u, /'/u, /\r/u ].freeze + SUBSTITUTES = ['&', '<', '>', '"', ''', ' '].freeze # Characters which are substituted in written strings - SLAICEPS = [ '<', '>', '"', "'", '&' ] - SETUTITSBUS = [ /</u, />/u, /"/u, /'/u, /&/u ] + SLAICEPS = [ '<', '>', '"', "'", '&' ].freeze + SETUTITSBUS = [ /</u, />/u, /"/u, /'/u, /&/u ].freeze # If +raw+ is true, then REXML leaves the value alone attr_accessor :raw @@ -27,7 +27,7 @@ class Text < Child (0x20..0xD7FF), (0xE000..0xFFFD), (0x10000..0x10FFFF) - ] + ].freeze VALID_XML_CHARS = Regexp.new('^['+ VALID_CHAR.map { |item| @@ -38,7 +38,7 @@ class Text < Child [item.first, '-'.ord, item.last].pack('UUU').force_encoding('utf-8') end }.join + - ']*$') + ']*$').freeze # Constructor # +arg+ if a String, the content is set to the String. If a Text, From fda69b7a8fd0c6af4cd33ab7e4cddbf167a904c1 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:40:39 -0700 Subject: [PATCH 04/11] Make FunctionsClass Ractor-compatible The @@available_functions class-variable wasn't frozen (to allow runtime methods to be added). I should be as fast to check for method_defined?(name, false), though this requires Ruby 2.6 --- lib/rexml/functions.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb index 8881c3fa..1446396c 100644 --- a/lib/rexml/functions.rb +++ b/lib/rexml/functions.rb @@ -8,8 +8,6 @@ module REXML # Therefore, in XML, "local-name()" is identical (and actually becomes) # "local_name()" class FunctionsClass - @@available_functions = {} - def initialize @context = nil @namespace_context = {} @@ -24,14 +22,7 @@ def initialize :context=, :get_namespace, :send, - ] - class << self - def method_added(name) - unless INTERNAL_METHODS.include?(name) - @@available_functions[name] = true - end - end - end + ].freeze def namespace_context=(x) ; @namespace_context=x ; end def variables=(x) ; @variables=x ; end @@ -422,7 +413,9 @@ def round( number ) end def send(name, *args) - if @@available_functions[name.to_sym] + name = name.to_sym + if self.class.method_defined?(name, false) and + !INTERNAL_METHODS.include?(name) super else # TODO: Maybe, this is not XPath spec behavior. From ba9fd675fc071c7df47cc3229d49312e76cd3c42 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:43:18 -0700 Subject: [PATCH 05/11] Make rexml/element.rb frozen_string_literal: true --- lib/rexml/element.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb index abdb28a7..6a34f99f 100644 --- a/lib/rexml/element.rb +++ b/lib/rexml/element.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true require_relative "parent" require_relative "namespace" require_relative "attribute" @@ -623,7 +623,7 @@ def namespace(prefix=nil) prefix = (prefix == '') ? 'xmlns' : prefix.delete_prefix("xmlns:") ns = namespaces[prefix] - ns = '' if ns.nil? and prefix == 'xmlns' + ns = +'' if ns.nil? and prefix == 'xmlns' ns end From b304f1638d29b19cac0994e6121cfe954740d3e3 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:43:30 -0700 Subject: [PATCH 06/11] Make rexml/xmldecl.rb frozen_string_literal: true --- lib/rexml/xmldecl.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rexml/xmldecl.rb b/lib/rexml/xmldecl.rb index d19407ce..0737f0ac 100644 --- a/lib/rexml/xmldecl.rb +++ b/lib/rexml/xmldecl.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true require_relative 'encoding' require_relative 'source' From 759e93059ddf52febfe30740869ce602519a0516 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Sun, 28 Jun 2026 19:43:46 -0700 Subject: [PATCH 07/11] Require Ruby 2.6 Needed for method_defined?(x, false) --- .github/workflows/test.yml | 4 +--- rexml.gemspec | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b2a36dd..73396077 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: uses: ruby/actions/.github/workflows/ruby_versions.yml@master with: engine: cruby-jruby - min_version: 2.5 + min_version: 2.6 inplace: needs: ruby-versions-inplace @@ -21,8 +21,6 @@ jobs: - macos-latest - windows-latest ruby-version: ${{ fromJson(needs.ruby-versions-inplace.outputs.versions) }} - exclude: - - {runs-on: macos-latest, ruby-version: 2.5} # include: # - runs-on: ubuntu-latest # ruby-version: truffleruby diff --git a/rexml.gemspec b/rexml.gemspec index e5cf8581..cdeb624a 100644 --- a/rexml.gemspec +++ b/rexml.gemspec @@ -57,5 +57,5 @@ Gem::Specification.new do |spec| spec.rdoc_options.concat(["--main", "README.md"]) spec.extra_rdoc_files = rdoc_files - spec.required_ruby_version = '>= 2.5.0' + spec.required_ruby_version = '>= 2.6.0' end From 6a1de801fb103865588d1559294b0fded937a438 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 29 Jun 2026 00:33:46 -0700 Subject: [PATCH 08/11] Freeze DEFAULT_ENTITIES --- lib/rexml/doctype.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rexml/doctype.rb b/lib/rexml/doctype.rb index d4f0bbe4..8886b5c2 100644 --- a/lib/rexml/doctype.rb +++ b/lib/rexml/doctype.rb @@ -59,7 +59,7 @@ class DocType < Parent 'lt'=>EntityConst::LT, 'quot'=>EntityConst::QUOT, "apos"=>EntityConst::APOS - } + }.freeze # name is the name of the doctype # external_id is the referenced DTD, if given @@ -180,7 +180,7 @@ def entity( name, expanding: nil ) def add child super(child) - @entities = DEFAULT_ENTITIES.clone if @entities == DEFAULT_ENTITIES + @entities = DEFAULT_ENTITIES.dup if @entities == DEFAULT_ENTITIES @entities[ child.name ] = child if child.kind_of? Entity end From 2e296eb43e9de7fa83c9c99799b3f53c6a1f7bbd Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 29 Jun 2026 00:36:05 -0700 Subject: [PATCH 09/11] Freeze entities --- lib/rexml/entity.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rexml/entity.rb b/lib/rexml/entity.rb index f55088fc..b939253b 100644 --- a/lib/rexml/entity.rb +++ b/lib/rexml/entity.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true require 'set' require_relative 'child' require_relative 'parseexception' @@ -127,7 +127,7 @@ def write out, indent=-1 # Returns this entity as a string. See write(). def to_s - rv = '' + rv = +'' write rv rv end @@ -138,14 +138,14 @@ def to_s # CAUTION: these entities does not have parent and document module EntityConst # +>+ - GT = Entity.new( 'gt', '>' ) + GT = Entity.new( 'gt', '>' ).freeze # +<+ - LT = Entity.new( 'lt', '<' ) + LT = Entity.new( 'lt', '<' ).freeze # +&+ - AMP = Entity.new( 'amp', '&' ) + AMP = Entity.new( 'amp', '&' ).freeze # +"+ - QUOT = Entity.new( 'quot', '"' ) + QUOT = Entity.new( 'quot', '"' ).freeze # +'+ - APOS = Entity.new( 'apos', "'" ) + APOS = Entity.new( 'apos', "'" ).freeze end end From 7efa4d4071c0b86c417660778c0e7321441f24fd Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 29 Jun 2026 02:08:59 -0700 Subject: [PATCH 10/11] More freezing --- lib/rexml/parsers/baseparser.rb | 11 ++++++----- lib/rexml/xmltokens.rb | 16 ++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index af8887e2..5498b711 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -134,11 +134,11 @@ class BaseParser EREFERENCE = /&(?!#{NAME};)/ DEFAULT_ENTITIES = { - 'gt' => [/>/, '>', '>', />/], - 'lt' => [/</, '<', '<', / [/"/, '"', '"', /"/], - "apos" => [/'/, "'", "'", /'/] - } + 'gt' => [/>/, '>', '>', />/].freeze, + 'lt' => [/</, '<', '<', / [/"/, '"', '"', /"/].freeze, + "apos" => [/'/, "'", "'", /'/].freeze + }.freeze module Private PEREFERENCE_PATTERN = /#{PEREFERENCE}/um @@ -157,6 +157,7 @@ module Private default_entities.each do |term| DEFAULT_ENTITIES_PATTERNS[term] = /&#{term};/ end + DEFAULT_ENTITIES_PATTERNS.freeze XML_PREFIXED_NAMESPACE = "http://www.w3.org/XML/1998/namespace" EXTERNAL_ID_PUBLIC_PATTERN = /\s+#{PUBIDLITERAL}\s+#{SYSTEMLITERAL}/um EXTERNAL_ID_SYSTEM_PATTERN = /\s+#{SYSTEMLITERAL}/um diff --git a/lib/rexml/xmltokens.rb b/lib/rexml/xmltokens.rb index 392b47b1..b74ad048 100644 --- a/lib/rexml/xmltokens.rb +++ b/lib/rexml/xmltokens.rb @@ -58,8 +58,8 @@ module XMLTokens "\\u0300-\\u036F", "\\u203F-\\u2040", ] - NAME_START_CHAR = "[#{name_start_chars.join('')}]" - NAME_CHAR = "[#{name_chars.join('')}]" + NAME_START_CHAR = "[#{name_start_chars.join('')}]".freeze + NAME_CHAR = "[#{name_chars.join('')}]".freeze NAMECHAR = NAME_CHAR # deprecated. Use NAME_CHAR instead. # From http://www.w3.org/TR/xml-names11/#NT-NCName @@ -70,13 +70,13 @@ module XMLTokens # # [5] NCNameChar ::= NameChar - ':' ncname_chars = name_chars - [":"] - NCNAME_STR = "[#{ncname_start_chars.join('')}][#{ncname_chars.join('')}]*" - NAME_STR = "(?:#{NCNAME_STR}:)?#{NCNAME_STR}" + NCNAME_STR = "[#{ncname_start_chars.join('')}][#{ncname_chars.join('')}]*".freeze + NAME_STR = "(?:#{NCNAME_STR}:)?#{NCNAME_STR}".freeze - NAME = "(#{NAME_START_CHAR}#{NAME_CHAR}*)" - NMTOKEN = "(?:#{NAME_CHAR})+" - NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*" - REFERENCE = "(?:&#{NAME};|&#\\d+;|&#x[0-9a-fA-F]+;)" + NAME = "(#{NAME_START_CHAR}#{NAME_CHAR}*)".freeze + NMTOKEN = "(?:#{NAME_CHAR})+".freeze + NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*".freeze + REFERENCE = "(?:&#{NAME};|&#\\d+;|&#x[0-9a-fA-F]+;)".freeze #REFERENCE = "(?:#{ENTITYREF}|#{CHARREF})" #ENTITYREF = "&#{NAME};" From 4ccac88ce6cac5fc2f03a557653d0281e475e77a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 3 Jul 2026 00:23:47 -0700 Subject: [PATCH 11/11] Explicitly unfreeze interpolated strings This fixes a FrozenError on Ruby 2.6/2.7. On Ruby 3.0+ interpolated strings are always unfrozen --- lib/rexml/element.rb | 2 +- lib/rexml/xmldecl.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb index 6a34f99f..d3446f7c 100644 --- a/lib/rexml/element.rb +++ b/lib/rexml/element.rb @@ -356,7 +356,7 @@ def initialize( arg = UNDEFINED, parent=nil, context=nil ) # e.inspect # => " ... " # def inspect - rv = "<#@expanded_name" + rv = +"<#@expanded_name" @attributes.each_attribute do |attr| rv << " " diff --git a/lib/rexml/xmldecl.rb b/lib/rexml/xmldecl.rb index 0737f0ac..a3cfb351 100644 --- a/lib/rexml/xmldecl.rb +++ b/lib/rexml/xmldecl.rb @@ -117,7 +117,7 @@ def content(enc) quote = "'" end - rv = "version=#{quote}#{@version}#{quote}" + rv = +"version=#{quote}#{@version}#{quote}" if @writeencoding or enc !~ /\Autf-8\z/i rv << " encoding=#{quote}#{enc}#{quote}" end