How to Create Custom haml_lint Linters
The majority of software engineering teams have rules, guidelines or standards for the code they write. While enforcing those manually can definitely work on small teams, it doesn't scale for medium to large teams. By automating this process, linters come to the rescue with their built-in rules. If this isn't enough for your team, custom linters allow you to codify anything not already covered by their default rules.
Today, you will learn how to create custom haml_lint linters for your Haml view templates in your Rails application.
What Are Linters?
In all kinds of projects, software engineers use linters to analyze source code to catch programming errors and bugs, detect coding style inconsistencies, enforce guidelines, measure code quality and much more. Web applications built with Rails aren't different! Every day, thousands of Rails applications rely on RuboCop to lint Ruby code. Same for JavaScript code with ESLint. Linting view templates is possible with erb_lint for ERB templates, haml_lint for Haml templates and slim_lint, you guessed it... for Slim!
So whenever linters find a potential issue in source code, they will report an offense and you need to either fix the issue or in rare cases, discard it as a false positive.
Why and How Would You Create a Custom Linter?
haml_lint provides a lot of useful linters, but you might have rules specific to your Rails application. In this case, you'll have to create a custom linter to enforce those rules.
To begin, add the gem haml_lint in your Gemfile under the development and test groups. Do it manually or with this command:
bundle add haml_lint --group=development,test
Let's now start with a simple example. In your Rails application, create the file my_first_linter.rb under the lib/haml_lint/ directory. Here's the code:
module HamlLint
# MyFirstLinter is the name of the linter in this example, but it can be anything
class Linter::MyFirstLinter < Linter
include LinterRegistry
# Report an offense if a `div` tag is used.
#
# @param [HamlLint::Tree::TagNode] a tag node in a Haml document
def visit_tag(node)
return unless node.tag_name == 'div'
record_lint(node, "You're not allowed divs!")
end
end
end
To use this linter, you need to enable it in your haml_lint configuration. If you haven't already configured haml_lint, create the file .haml_lint.yml at the root of your Rails application. Those are the lines to add in your haml_lint configuration:
require:
- './lib/haml_lint/my_first_linter.rb'
linters:
MyFirstLinter:
enabled: true
So from now on, whenever running haml_lint, this linter will report an
offense for every <div>
defined in a Haml view template. This isn't the most
useful linter, but it's only the beginning!
A Second Example
Let's have a look at another linter which reports an offense if the instance
variable @pagetitle
is not set in a Haml view template. Again, same
procedure as with the first linter. Under the lib/haml_lint/ directory, create
a file set_pagetitle_in_view_linter.rb with the following code:
module HamlLint
class Linter::SetPagetitleInView < Linter
include LinterRegistry
# Report an offense if the instance variable @pagetitle is not set in a
# Haml view. Partials are ignored by this linter.
#
# @param [HamlLint::Tree::RootNode] the root of a syntax tree
def visit_root(root_node)
# Do not proceed if the view isn't under the directory 'app/views/'
# and doesn't end with the extension '.html.haml'
return unless root_node.file.match?(%r{^app/views/.*\.html\.haml$})
# Do not proceed if the view is a partial.
# Only partials start with an underscore.
return if File.basename(root_node.file).start_with?('_')
# Do not proceed if the view defines the instance variable @pagetitle,
# then this rule is respected. Yay!
return if instance_variable_pagetitle_is_defined?(document)
record_lint(root_node, 'Set the instance variable @pagetitle to have a ' \
'page title when the view is rendered.')
end
private
# @param [HamlLint::Document] a parsed Haml document and its associated metadata
def instance_variable_pagetitle_is_defined?(document)
ruby_source = HamlLint::RubyExtractor.new.extract(document).source
parsed_ruby = HamlLint::RubyParser.new.parse(ruby_source)
parsed_ruby.each_descendant.find do |descendant_node|
# Details on Abstract Syntax Tree from Parser gem:
# https://github.com/whitequark/parser/blob/11c7644365fe554217bb4670a4cbc905ab8504cd/doc/AST_FORMAT.md#to-instance-variable
# Are we assigning an instance variable? Is it called @pagetitle?
descendant_node.ivasgn_type? && descendant_node.children.first == :@pagetitle
end
end
end
end
Enable this linter in your haml_lint configuration file. This should be the content of .haml_lint.yml:
require:
- './lib/haml_lint/my_first_linter.rb'
- './lib/haml_lint/set_pagetitle_in_view_linter.rb'
linters:
MyFirstLinter:
enabled: true
SetPagetitleInView:
enabled: true
With this example of a project-specific rule codified in a linter, it's now
impossible to forget assigning a value to @pagetitle
in a view template. Gone
are the "You forgot to set @pagetitle
in view_123.html.haml." reviews in a
pull request since running haml_lint will catch this issue. This is the power
of automation!
A Deeper Look Into How haml_lint Linters Work
To really understand how haml_lint linters work, you need to learn a few things. Nothing complicated, but without this knowledge, you won't be able to create your own linters.
Different linters will analyze code in different ways by processing different
nodes. In haml_lint's world, this is called visiting a node and it is defined
in the various visit_*
methods like visit_tag
or visit_root
.
Going back to the two custom linters you've seen so far, the first visits HTML
tag nodes and checks if they are a <div>
. The second linter is visiting root
nodes, those are complete Haml view templates, and verify if the instance
variable @pagetitle
is set. Have a look at haml_lint's
code
for a list of all nodes. Every node has its visit_NODE_NAME
method. So
visit_root
is for root nodes as you've already seen, visit_script
is for
script nodes and so on.
To evaluate any Ruby code included in Haml view templates, haml_lint
relies on the parser gem to build an
abstract syntax tree. With
this, analyzing Ruby code is also possible in custom linters. It's exactly
what the second linter SetPagetitleInView
does when verifying if @pagetitle
is set. While most linters probably won't need to go this deep, it's definitely
useful to know how.
For more linter examples, have a look at the linters included in haml_lint. This will definitely help you understand even better how linters are built.
Lint All The Things!
It's now time for you to create custom haml_lint linters for Haml view templates in your Rails applications.