Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ permissions:

jobs:
build:
name: "Test / Ruby ${{ matrix.ruby }}"
name: "Test / Ruby ${{ matrix.ruby }}${{ matrix.frozen-string-literal && ' (frozen string literals)' || '' }}"
runs-on: ubuntu-latest
strategy:
matrix:
ruby:
- "3.3"
- "3.4"
- "4.0"
frozen-string-literal: [false]
include:
- ruby: "4.0"
frozen-string-literal: true
fail-fast: false

steps:
Expand Down Expand Up @@ -57,6 +61,8 @@ jobs:
python -m pip install --require-hashes --no-deps -r /tmp/requirements.txt

- name: Run rake
env:
RUBYOPT: ${{ matrix.frozen-string-literal && '--enable=frozen-string-literal' || '' }}
run: |
export PATH=$PATH:/.perl6/bin:/opt/rakudo-pkg/bin
bundle exec rake
3 changes: 2 additions & 1 deletion lib/github/markup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def render_s(symbol, content, options: {})
end

def markup(symbol, gem_name, regexp, languages, opts = {}, &block)
impl = GemImplementation.new(regexp, languages, gem_name, &block)
impl = GemImplementation.new(regexp, languages, gem_name,
mutable_string_literals: opts.fetch(:mutable_string_literals, false), &block)
markup_impl(symbol, impl)
end

Expand Down
30 changes: 28 additions & 2 deletions lib/github/markup/gem_implementation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ module Markup
class GemImplementation < Implementation
attr_reader :gem_name, :renderer

def initialize(regexp, languages, gem_name, &renderer)
def initialize(regexp, languages, gem_name, mutable_string_literals: false, &renderer)
super(regexp, languages)
@gem_name = gem_name.to_s
@mutable_string_literals = mutable_string_literals
@renderer = renderer
end

def load
return if defined?(@loaded) && @loaded
require gem_name
if @mutable_string_literals
with_mutable_string_literals { require gem_name }
else
require gem_name
end
@loaded = true
end

Expand All @@ -25,6 +30,27 @@ def render(filename, content, options: {})
def name
gem_name
end

private

# Some renderer gems modify string literals in place. When string literals
# are frozen by default (--enable=frozen-string-literal, or a future Ruby),
# compile those gems with mutable literals so they keep working. Otherwise
# leave the compile options alone: Ruby can't restore its default
# "chilled" state once it has been changed.
def with_mutable_string_literals
# :nocov:
return yield unless defined?(RubyVM::InstructionSequence)
# :nocov:
options = RubyVM::InstructionSequence.compile_option
return yield unless options[:frozen_string_literal]
begin
RubyVM::InstructionSequence.compile_option = options.merge(frozen_string_literal: false)
yield
ensure
RubyVM::InstructionSequence.compile_option = options
end
end
end
end
end
6 changes: 3 additions & 3 deletions lib/github/markups.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@

GitHub::Markup.markup_impl(::GitHub::Markups::MARKUP_RDOC, GitHub::Markup::RDoc.new)

GitHub::Markup.markup(::GitHub::Markups::MARKUP_ORG, 'org-ruby', /org/, ["Org"]) do |filename, content, options: {}|
GitHub::Markup.markup(::GitHub::Markups::MARKUP_ORG, 'org-ruby', /org/, ["Org"], mutable_string_literals: true) do |filename, content, options: {}|
Orgmode::Parser.new(content, {
:allow_include_files => false,
:skip_syntax_highlight => true
}).to_html
end

GitHub::Markup.markup(::GitHub::Markups::MARKUP_CREOLE, :creole, /creole/, ["Creole"]) do |filename, content, options: {}|
GitHub::Markup.markup(::GitHub::Markups::MARKUP_CREOLE, :creole, /creole/, ["Creole"], mutable_string_literals: true) do |filename, content, options: {}|
Creole.creolize(content)
end

GitHub::Markup.markup(::GitHub::Markups::MARKUP_MEDIAWIKI, :wikicloth, /mediawiki|wiki/, ["MediaWiki"]) do |filename, content, options: {}|
GitHub::Markup.markup(::GitHub::Markups::MARKUP_MEDIAWIKI, :wikicloth, /mediawiki|wiki/, ["MediaWiki"], mutable_string_literals: true) do |filename, content, options: {}|
wikicloth = WikiCloth::WikiCloth.new(:data => content)
WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS << 'tt' unless WikiCloth::WikiBuffer::HTMLElement::ESCAPED_TAGS.include?('tt')
wikicloth.to_html(:noedit => true)
Expand Down
54 changes: 54 additions & 0 deletions test/coverage_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ def test_mediawiki_render_is_idempotent_for_escaped_tags
assert_equal 1, count_after_second, "second render must not append a duplicate 'tt'"
end

# --- gem_implementation.rb mutable_string_literals ---------------------

def test_mutable_string_literals_unfreezes_literals_while_the_gem_loads
probe = File.expand_path("fixtures/compile_option_probe", __dir__)
impl = GitHub::Markup::GemImplementation.new(/covmutable/, [], probe, mutable_string_literals: true)
with_fake_compile_option(frozen_string_literal: true) do |writes|
impl.load
assert_equal false, COMPILE_OPTION_PROBE, "the gem should load with mutable string literals"
assert_equal [false, true], writes.map { |options| options[:frozen_string_literal] }
end
end

def test_mutable_string_literals_restores_the_option_when_the_gem_fails_to_load
impl = GitHub::Markup::GemImplementation.new(/covmissing/, [], "covmissing-not-a-gem", mutable_string_literals: true)
with_fake_compile_option(frozen_string_literal: true) do |writes|
assert_raises(LoadError) { impl.load }
assert_equal [false, true], writes.map { |options| options[:frozen_string_literal] }
end
end

def test_mutable_string_literals_leaves_the_default_chilled_mode_alone
impl = GitHub::Markup::GemImplementation.new(/covchilled/, [], "shellwords", mutable_string_literals: true)
with_fake_compile_option(frozen_string_literal: nil) do |writes|
impl.load
assert_empty writes
end
end

# --- command_implementation.rb block arity branches --------------------

def test_command_block_with_arity_two_receives_rendered_and_content
Expand Down Expand Up @@ -361,6 +389,32 @@ def with_stub_const(path, value)
parent.const_set(name, original) if had_const
end

# Replaces RubyVM::InstructionSequence.compile_option and compile_option= with
# a fake, so a test can simulate frozen string literals without touching the
# real compile options (Ruby can't restore its default "chilled" state).
# Yields the list of values written to compile_option=.
def with_fake_compile_option(options)
iseq = RubyVM::InstructionSequence
getter = iseq.method(:compile_option)
setter = iseq.method(:compile_option=)
current = getter.call.merge(options)
writes = []
redefine_quietly(iseq, :compile_option) { current }
redefine_quietly(iseq, :compile_option=) { |value| writes << value; current = value }
yield writes
ensure
redefine_quietly(iseq, :compile_option, getter)
redefine_quietly(iseq, :compile_option=, setter)
end

# define_singleton_method without the "method redefined" warning.
def redefine_quietly(object, name, body = nil, &block)
verbose, $VERBOSE = $VERBOSE, nil
object.define_singleton_method(name, body || block)
ensure
$VERBOSE = verbose
end

def without_linguist
had_const = Object.const_defined?(:Linguist, false)
original = Object.const_get(:Linguist) if had_const
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/compile_option_probe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Records the frozen_string_literal compile option in effect while this file is required.
COMPILE_OPTION_PROBE = RubyVM::InstructionSequence.compile_option[:frozen_string_literal]