qdrt3g
Last Updated: March 07, 2016
·
2.52K
· jamesmartin
Me coding in regents park

Getting Empirical About Complexity in Templates

Recently, we noticed that some parts of our Rails application had become difficult to work with. A brutal push towards a looming date with a production system had worn our refactoring resolve and there were parts of the code that were looking decidedly crufty.

Most people seemed to have an intuitive sense of the classes that needed the most attention; those that most recently caused them pain. The big, painful classes play on our emotions and tend to stick in the mind, becoming natural refactoring targets. But are they always the best place to start?

Michael Feathers wrote an interesting piece for Sticky Minds, encouraging us to get empirical about refactoring, in which he describes the “churn vs. complexity” graph that can reveal volatile areas of a code base, and serve as a divining rod for where to begin refactoring right now.

I dutifully ran Chad Fowler’s Turbulence tool over our code base, which showed the big, hairy classes that we all intuitively knew needed to be refactored, along with some other less obvious targets that we hadn’t felt pain from recently.

However, a nagging doubt remained in my head. Some of the most complex, painful code that I had recently worked on was not shown in the Turbulence report because that code resided in ERB templates, which Turbulence cannot process.

So I set out on a spiritual journey, to see how our ERB templates compared to some of the big, scary Ruby classes in our code base (yes, they were mostly out-of-control ActiveRecord models). Performing static analysis on ERB files is a non-trivial, finicky task and I can understand why it was left out of Flog, the Ruby static analysis, complexity reporter. Given the sorry state of Ruby code parsing (it’s a really tricky language), the following stern warning from Flog when presented with ERB templates seems harsh but entirely fair:

warn "\n...stupid lemmings and their bad erb templates... skipping"

Undeterred by Flog’s exhortations, I pressed ahead with my spirit quest, into the murky (murky) world of Ruby parsers…

Time passes. I’ll spare you the gory details.

By and by I came up with this little script:

require 'action_view'
require 'ostruct'
require 'erb'
require 'flog'
require 'ruby_parser'
require 'sexp_processor'

ERBHandler = ActionView::Template::Handlers::ERB.new

def new_template(body, details={format: :html})
  ActionView::Template.new(body, "irrelevant", details.fetch(:handler) {ERBHandler}, details)
end

def sort_by_combined_score(totals)
  totals.sort do |a, b|
    b[1].values.inject(&:+) <=> a[1].values.inject(&:+)
  end
end

if $0 == __FILE__
  src_dir = ARGV[0]
  raise ArgumentError, "Supply a path containing ERB templates" unless src_dir

  puts "Contemplating ERB templates from: #{src_dir}"

  erb_files = Dir.glob(File.join(src_dir, "**/*.erb"))

  totals = erb_files.inject({}) do |totals, file|
    erb_source = File.read(file)
    ruby_source = ERBHandler.call(new_template(erb_source))
    sexp_parsed = RubyParser.new.parse(ruby_source)

    flog = Flog.new
    flog.process(sexp_parsed)

    totals[file] = flog.totals
    totals
  end

  sort_by_combined_score(totals).each do |filename, totals|
    puts "#{filename} : #{totals}"
  end

end

There is some devilry here that may be worth explaining, for the curious.

Initially, I tried the simplest thing that could possibly work: Slurp up a directory full of ERB templates, parse them with the RubyParser and feed the resulting Ruby code to Flog for analysis. However, because Rails-flavored ERB templates depend on all manner of Rails-related paraphernalia, the RubyParser chokes on various unknown symbols. It transpires that Rails does a pre-rendering step; escaping special Rails-esque directives and properly accounting for various template encodings before passing the resulting “sanitized” template to the appropriate rendering engine.

In order to fool the delicate RubyParser into parsing our Rails-riddled templates, we first wrap them in a new ActionView::Template, which performs all the necessary Rails sanitization.

Next we use the ERBHandler to produce pure, although basically unreadable, Ruby code from our sanitized Rails template, which we pass to the RubyParser resulting in clean, floggable Ruby code, or at least a symbolic expression of that code.

From here we pass our sexp to Flog (Mmm.), which produces the complexity score.

Finally, we compile a sorted list of each file and its complexity and print out the results for our edification.

On our Rails code base, this clearly identified the most complex ERB templates, most of which weren’t surprising but served as a great talking point among the team when prioritizing refactoring.

Some of our ERB templates stacked up against our scariest Ruby classes in terms of complexity. I think equal or greater attention should be paid to these silent killers, because they’re frequently maintained by both the self-identifying “programmers” on the team and also the “designers”. There's a lot of delicate little fingers poking around in those gears.

One of the benefits of starting to refactor from a template, which for a web app is often the client of your big scary Ruby objects, is that it tends to push beneficial change into those dependencies, pointing the way for further refactoring and simplification.

A common reaction when I started investigating the possibility of measuring the complexity of templates was that “views should contain no logic” and while I agree with this whole-heartedly in principle, I know from experience working on real Rails projects, with real people, that in practice templates tend to be complexity magnets.

It’s easy to dismiss an approach to improvement because it violates an ideal you hold dear — “no logic in views”, “nil is evil”, “no method longer than two lines” etc. But be careful; sometimes idealism, regardless of good intentions, can kill somebody’s motivation to make any improvement at all.

When you encounter enthusiasm be gentle with it, for it is a fragile thing.

Say Thanks
Respond