The Perils of Accidental Global Includes in Ruby

October 14, 2016

We deploy our front-end code to production multiple times per day. Our ability to do this is closely tied to our confidence in our test suite and deployment infrastructure. Tens of thousands of tests are run against our code whenever we click the deploy button. Given that this is the case, when something happens that causes us to lose confidence in the reliability of our tests, it is important that we fix it as soon as possible.

Over the years, we have collected a few Ruby on Rails helpers with utility methods that we’ve found ourselves reusing throughout the application. For example, we have FormatHelper, which contains methods for formatting numbers in a variety of different ways; BrowserHelper, to aid in browser version and capability detection; DisclosuresHelper, which provides text for various legal and regulatory disclosures; and many more.

Recently, we added a new class that required a couple helper methods from one of these modules, so the following code was added:

The effect of including the module is that its methods are made available to instances of MyClass, but MyClass is attempting to use it in a class method. What we should have done is extend FormatHelper:

All code we write is thoroughly tested, and the code in question was no exception:

In our class, we incorrectly extended FormatHelper rather than including it. This had the effect of adding the phone_number helper method to instances of the class rather than the class itself. We would expect the above test to catch this error and fail. However, when we ran this test it passed – but why? Our first guess was that the module was still being included somehow/somewhere. We can find out how and where a module is being included by taking advantage of the Module.included callback, which is invoked whenever the module is included somewhere.

And right there at the top of the file ‘blueprints.rb’ is the culprit:

The FormatHelper module was being included in this file, blueprints.rb, while in the global scope, but why did this cause our test to still pass?

A brief segue into Ruby main

When you load up irb, by default you are in the global scope, also known as ‘main’. Since everything is an object in ruby, ‘main’ is too – it is an instance of the ‘Object’ class with the added special property of methods being added to it (or modules included) also being added the ‘Object’ class itself. The following diagram shows a simplified snapshot of the Ruby hierarchy in the context of the issue we investigated:

Ruby object hierarchy

The Ruby class ‘Object’ is near the top of the Ruby hierarchy, and any changes to the ‘Object’ class will affect any classes that inherit from it, which is pretty much all Ruby objects.

Back to our regularly scheduled programming

blueprints.rb defines test fixtures that are loaded as part of our Rails test environment initialization. If you recall that blueprints.rb included the MyHelper module at the top of the file and therefore in the global scope, every single class and object would always have access to all of the methods in the module, whether that module is being included correctly in a specific file or not. However, this didn’t happen in production, because the blueprints.rb file is only loaded in the test environment and therefore the module wasn’t globally included. The end result is what we saw previously: a test passing when it should be failing.

In order to fix this, I needed to ensure that the module wasn’t included globally. However, since the helpers are used later in the file I couldn’t simply remove it.

My first idea was to include the module in Person, so the scope of all of the loaded methods would be within that class.

Unfortunately that didn’t work, because when a block is yielded it executes in the same scope it was created in. The FormatHelper methods were included in the scope of the Person class, but the block was executed within the global scope.

My next thought was to include the module within the block itself.

This didn’t work either, because blocks in Ruby inherit the scope they were created in. This block is in the main scope, so including a module here added all of the module methods to the global scope.

One other option I had was to change the module to use module functions, like so:

However, the result of this would be that these helpers could then only be called directly on the module rather than being included and called on the instance. They are commonly used as mixins throughout the codebase and we don’t particularly want to lose that ability. However, we can get a combination of both module and mixin functions:

This had the effect of adding instance methods onto the eigenclass of FormatHelper, callable via FormatHelper.phone_number, while still allowing FormatHelper to be included as a regular module to apply its methods on to instances of other classes.

Now, when we’re in the global scope (e.g. in our blueprints.rb file), these methods can be called via:

And they can still be used as mixins where applicable:

The original, immediate problem is now resolved – our original tests are failing correctly, and we can again be confident in our test suite and therefore our continuous deployment process. However, it is critically important to not just fix the symptom, but also to investigate and resolve the cause. From this investigation, we can pretty readily infer that global includes are bad, so we should ensure they don’t happen: code reviewers shouldn’t have to be on the lookout for global includes in pull requests, so we now have a custom RuboCop to lint for global includes and fail offending builds accordingly.