Adam Prescott

Removing work-arounds automatically

Every now and then, a library you’re using just doesn’t quite work. There’s a bug and a fix hasn’t been made available yet. You can add a work-around, but you can also automate its removal.

A simple example is Liquid’s date filter. It used to be possible to do this:

2017

The output would be the current year as a number, such as 2013. Then for some reason it stopped working. The reason was that in Ruby 1.8.7, you could pass "now" to Time.parse, and the Time library in stdlib would do what you expect.

require "time"
Time.parse("now")
#=> Thu Mar 14 16:33:05 +0000 2013

It stopped working because later versions of Ruby don’t allow it.

require "time"
Time.parse("now")
#=> ArgumentError: no time information in "now"

You definitely don’t want to patch Time.parse itself — that’s a costly assumption to add — but being able to use "now" for Liquid’s date filter to fix the regression is useful, and it’s something they’ll likely fix anyway. You can patch it easily enough yourself.

module Liquid::StandardFilters
  alias_method :date_orig, :date

  def date(input, format)
    input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
  end
end

All is mostly well. But this type of patch is meant to be temporary.

If the problem is resolved upstream — in this case, if support for "now" is merged and released into a new version — the work-around is useless clutter. Worse, for non-trivial patches, your code will be buggy, and thus worse than useless: the library maintainers will have dealt with edge cases you can’t foresee. So you’re now maintaining a very “tiny” private fork through one or more patches, and the code is useless.

How do you deal with that? It’s easy. You just remember every work-around you’ve added and stay aware of all changes to all libraries you’ve patched. Then you’ll know when to remove the patch when it’s no longer needed.

Or you can automate it with a couple of good principles.

  1. Make it become redundant automatically.
  2. Ensure you automatically find out when it’s redundant.

Let’s return to the date example to see what this means concretely.

Automatic redundancy

The original stimulus in adding the patch was, in this case, because 2017 wasn’t working. So the patch needs to become redundant when support is once again baked in. Wrap it in a conditional:

if !date_is_supported
  module Liquid::StandardFilters
    alias_method :date_orig, :date

    def date(input, format)
      input == "now" ? date_orig(Time.now, format) : date_orig(input, format)
    end
  end
end

Leaving aside the details of how date_is_supported works, as soon as Liquid’s original date filter starts accepting "now", the monkey-patch automatically becomes dead code.

Redundancy alert

Since the code will, in theory, become totally dead eventually, it’d be nice to find out when it can be removed. The easiest way you can do that is to add an automated negative test against the functionality you’re adding, before any of your code runs — and thus before any possible monkey-patching.

describe "date 'now' patch" do
  # if this test fails, the monkey match on StandardFilters#date can be removed
  it "is necessary" do
    liquid_filter = Object.new
    liquid_filter.extend(Liquid::StandardFilters)
    liquid_filter.date("now", "%Y").should_not == Time.now.year.to_s
  end
end

This is RSpec, but it doesn’t matter what the test looks like. Note that the test is explicitly asserting a negative. You could also add the patch in all cases, and then test against date_orig.

Now, as soon as Liquid supports "now", not only will your code become dead, but the test will fail and you’ll know it can be removed.

If a failing test causes problems, you can go for simply printing a warning.

Another example — Redcarpet

Here’s another example which fixes a regression bug in Redcarpet to use single quote characters. It’s better than the above because the work-around is less trivial.

renderer = Redcarpet::Markdown.new(Redcarpet::Render::SmartyHTML)
renderer.render("foo's bar")

The issue is that the SmartyHTML renderer incorrectly turns input like "foo's bar" into

<p>foo&#39;s bar</p>

when it should instead use a curly quote from the Smartypants processing:

<p>foo&rsquo;s bar</p>

The problem is caused by SmartyHTML not looking for HTML-escaped entities like &#39; as equivalent to '. (Double quotes work fine.)

So there is a work-around, to substitute and reprocess.

def markdown(body)
  renderer = Redcarpet::Markdown.new(Redcarpet::Render::SmartyHTML)
  html = renderer.render(body).strip

  # make sure we aren't overriding unless we need to.
  # causes the workaround to automatically turn off.
  if !renderer.render("a 'quoted' word").include?("&rsquo;")
    # fix the broken single curly quotes by putting them back
    # as unescaped characters and then re-running the renderer.
    html.gsub!("&#39;", "'")
    html = renderer.render(html)
  end

  html
end

As before, the work-around is dependent on a condition that checks if we even need it. If not, don’t bother.

In this case, the patch is much more iffy:

Again, the corresponding check:

describe "curly quote patch" do
  # if this test fails, the workaround for the "markdown" filter can be removed
  it "is necessary" do
    renderer = Redcarpet::Markdown.new(Redcarpet::Render::SmartyHTML)
    renderer.render("something's here").should_not include("&rsquo;s")
  end
end

Since the patch is self-removing, these problems disappear once SmartyHTML is fixed upstream and others update their version of Redcarpet. And eventually the test suite fails, letting you know it’s time to cut out the work-around.