When an Array is not an Array

The story of a simple array sort gone wrong

Part of the release process of the Foreman project includes extracting all strings from the source code for translation, and pulling updated translations from an online service we use called Transifex. As part of the release process for version 1.19.0, I ran the rake task which handles this step.

Unfortunately, it failed with a strange error message:

ArgumentError: comparison of Array with Array failed
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/po.rb:270:in `sort_by’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/po.rb:270:in `sort_by_msgid’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/po.rb:227:in `sort’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/po.rb:208:in `to_s’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/tools/msgcat.rb:57:in `run’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/tools/msgcat.rb:32:in `run’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext-3.2.9/lib/gettext/tools/task.rb:391:in `block in define_po_file_task’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/gettext_i18n_rails-1.8.0/lib/gettext_i18n_rails/tasks.rb:65:in `block (2 levels) in <top (required)>’
/home/tbrisker/.rvm/gems/ruby-2.5.1@foreman/gems/rake-12.3.1/exe/rake:27:in `<top (required)>’

Well, let’s look at the failing function’s code:

Hmm… That’s quite odd, looks like a fairly simple sorting function. Looking at the gem’s git history indicates that this function hasn’t changed in ages — so why is it failing now? The comment seems pretty cryptic, and without digging too much into the gem’s internals it would be difficult to understand what it means. Time to pull out the ol’ debugger and stick a binding.pry breakpoint inside the function to try and understand what is going on here.

This is what one of the msgid_entry objects looks like:

It is an array that is composed of two elements — the first, an array that has two element itself, and the second is a GetText::POEntry object, that is some sort of internal gettext object representing the translation. Now the comment in the code makes a bit more sense — the array contains a message context (in this case, nil) and a message ID, in this case, the string "Failed to fetch:”. The sorting function sorts the entries by the values of this array ( msgid_entry[0]). So for some reason, one (or more) of the entries contains a problematic value in this array.

Stepping a few iterations in the loop didn’t cause the exception to be raised, and considering there are over 3,000 entries in the list (presumably, one for every string that needs translation), manually finding the problematic entry can be painstaking.

After a while of playing around with conditional breakpoints, the culprit was found:

This was the only entry that had something other than nil for the first element of the first array — the message context. To make my work easier, the second element included the reference to the exact line of code where the string came from.

Looking at the code, the fix was clear — someone had a typo and used the incorrect method for marking the string for translation. So instead of using the method needed to mark the string as one that requires both singular and plural translation, the regular translation method was called, which apparently accepts an additional parameter for the context.

With the issue found and fixed, one question remained — why did this fail with the cryptic “comparison of Array with Array failed” error message?

To understand this, we need to dive a bit into how Ruby’s sort_by function works. Simply put, it accepts a block that gets as its argument the elements in the sorted object, and sorts them by the return value of the block. In our case, the entries are sorted by the value of the first element in each entry — the array containing the message context and ID.

But how does Ruby sort arrays? Turns out it sorts them by comparing one element at a time. To compare the elements, it calls the function <=>, which returns -1, 0, or 1 that indicate if the first element is smaller, equal or larger than the other. However, it can also return nil if the two elements can’t be compared, which is exactly what is happening here.

To illustrate, let’s look at what happens when we compare two strings:

[1] pry(main)> 'a' <=> 'foo'
=> -1

Since a is lexically smaller than foo, the function returns -1. But what happens when we try to compare a string with nil?

[2] pry(main)> nil <=> 'foo'
=> nil

As expected, since Ruby doesn’t know how to compare the two different object types, it returns nil. But what does that have to do with our error? Well, if you recall, Ruby compares arrays by iterating over the array elements. Now lets see what happens if it tries to compare arrays containing elements that can’t be compared:

[3] pry(main)> [[nil, 'foo'], ['bar', 'baz']].sort
ArgumentError: comparison of Array with Array failed
from (pry):4:in `sort'

One of the arrays being sorted by has a string for its first element, while the other has nil. This means Ruby doesn’t know how to sort these arrays, leading to an error. Unsurprisingly, since this is exactly the case as we were hitting with our translations, with one array containing a string where all others had nil, the same error was raised as when we tried the sorting in this example.