Skip to content

Shortcuts

Shortcuts are defined on a Fastfile inside any ruby project.

Use ~/Fastfile

You can also add one extra in your $HOME if you want to have something loaded always.

By default, the command line interface does not load any Fastfile if the first param is not a shortcut. It should start with ..

I'm building several researches and I'll make the examples open here to show several interesting cases in action.

List your fast shortcuts

As the interface is very rudimentar, let's build a shortcut to print what shortcuts are available. This is a good one to your $HOME/Fastfile:

# List all shortcut with comments
Fast.shortcut :shortcuts do
  fast_files.each do |file|
    lines = File.readlines(file).map{|line|line.chomp.gsub(/\s*#/,'').strip}
    result = capture_file('(send ... shortcut $(sym _', file)
    result = [result] unless result.is_a?Array
    result.each do |capture|
      target = capture.loc.expression
      puts "fast .#{target.source[1..-1].ljust(30)} # #{lines[target.line-2]}"
    end
  end
end

And using it on fast project that loads both ~/Fastfile and the Fastfile from the project:

fast .version       # Let's say you'd like to show the version that is over the version file
fast .parser        # Simple shortcut that I used often to show how the expression parser works
fast .bump_version  # Use `fast .bump_version` to rewrite the version file
fast .shortcuts     # List all shortcut with comments

Search for references

I always miss bringing something simple as grep keyword where I can leave a simple string and it can search in all types of nodes and report interesting things about it.

Let's consider a very flexible search that can target any code related to some keyword. Considering that we're talking about code indentifiers:

# Search all references about some keyword or regular expression
Fast.shortcut(:ref) do
  require 'fast/cli'
  Kernel.class_eval do
    def matches_args? identifier
      search = ARGV.last
      regex = Regexp.new(search, Regexp::IGNORECASE)
      case identifier
      when Symbol, String
        regex.match?(identifier) || identifier.to_s.include?(search)
      when Astrolabe::Node
        regex.match?(identifier.to_sexp)
      end
    end
  end
  pattern = <<~FAST
    {
      ({class def sym str} #matches_args?)'
      ({const send} nil #matches_args?)'
    }
  FAST
  Fast::Cli.run!([pattern, '.', '--parallel'])
end

Rails: Show validations from models

If the shortcut does not define a block, it works as a holder for arguments from the command line.

Let's say you always use fast "(send nil {validate validates})" app/models to check validations in the models. You can define a shortcut to hold the args and avoid retyping long lines:

# Show validations from app/models
Fast.shortcut(:validations, "(send nil {validate validates})", "app/models")

And you can reuse the search with the shortcut starting with a .:

fast .validations

And it will also accept params if you want to filter a specific file:

fast .validations app/models/user.rb

Note that you can also use flags in the command line shortcuts

Let's say you also want to use fast --headless you can add it to the params:

Fast.shortcut(:validations, "(send nil {validate validates})", "app/models", "--headless")

Automated Refactor: Bump version

Let's start with a real usage to bump a new version of the gem.

Fast.shortcut :bump_version do
  rewrite_file('(casgn nil VERSION (str _)', 'lib/fast/version.rb') do |node|
    target = node.children.last.loc.expression
    pieces = target.source.split('.').map(&:to_i)
    pieces.reverse.each_with_index do |fragment, i|
      if fragment < 9
        pieces[-(i + 1)] = fragment + 1
        break
      else
        pieces[-(i + 1)] = 0
      end
    end
    replace(target, "'#{pieces.join('.')}'")
  end
end

And then the change is done in the lib/fast/version.rb:

module Fast
-  VERSION = '0.1.6'
+  VERSION = '0.1.7'
end

RSpec: Find unused shared contexts

If you build shared contexts often, probably you can forget some left overs.

The objective of the shortcut is find leftovers from shared contexts.

First, the objective is capture all names of the RSpec.shared_context or shared_context declared in the spec/support folder.

Fast.capture_all('(block (send {nil,_} shared_context (str $_)))', Fast.ruby_files_from('spec/support'))

Then, we need to check all the specs and search for include_context usages to confirm if all defined contexts are being used:

specs = Fast.ruby_files_from('spec').select{|f|f !~ %r{spec/support/}}
Fast.search_all("(send nil include_context (str #register_usage)", specs)

Note that we created a new reference to #register_usage and we need to define the method too:

@used = []
def register_usage context_name
    @used << context_name
end

Wrapping up everything in a shortcut:

# Show unused shared contexts
Fast.shortcut(:unused_shared_contexts) do
  puts "Checking shared contexts"
  Kernel.class_eval do
    @used = []
    def register_usage context_name
      @used << context_name
    end
    def show_report! defined_contexts
      unused = defined_contexts.values.flatten - @used
      if unused.any?
        puts "Unused shared contexts", unused
      else
        puts "Good job! all the #{defined_contexts.size} contexts are used!"
      end
    end
  end
  specs = ruby_files_from('spec/').select{|f|f !~ %r{spec/support/}}
  search_all("(send nil include_context (str #register_usage)", specs)
  defined_contexts = capture_all('(block (send {nil,_} shared_context (str $_)))', ruby_files_from('spec'))
  Kernel.public_send(:show_report!, defined_contexts)
end

Why #register_usage is defined on the Kernel?

Yes! note that the #register_usage was forced to be inside Kernel because of the shortcut block that takes the Fast context to be easy to access in the default functions. As I can define multiple shortcuts I don't want to polute my Kernel module with other methods that are not useful.

RSpec: Remove unused let

First shortcut with experiments

If you're not familiar with automated experiments, you can read about it here.

The current scenario is similar in terms of search with the previous one, but more advanced because we're going to introduce automated refactoring.

The idea is simple, if it finds a let in a RSpec scenario that is not referenced, it tries to experimentally remove the let and run the tests:

# Experimental remove `let` that are not referenced in the spec
Fast.shortcut(:exp_remove_let) do
  require 'fast/experiment'
  Kernel.class_eval do
    file = ARGV.last

    defined_lets = Fast.capture_file('(block (send nil let (sym $_)))', file).uniq
    @unreferenced= defined_lets.select do |identifier|
      Fast.search_file("(send nil #{identifier})", file).empty?
    end

    def unreferenced_let?(identifier)
      @unreferenced.include? identifier
    end
  end

  experiment('RSpec/RemoveUnreferencedLet') do
    lookup ARGV.last
    search '(block (send nil let (sym #unreferenced_let?)))'
    edit { |node| remove(node.loc.expression) }
    policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
  end.run
end

And it will run with a single file from command line:

fast .exp_remove_let spec/my_file_spec.rb

FactoryBot: Replace create with build_stubbed

For performance reasons, if we can avoid touching the database the test will always be faster.

# Experimental switch from `create` to `build_stubbed`
Fast.shortcut(:exp_build_stubbed) do
  require 'fast/experiment'
  Fast.experiment('FactoryBot/UseBuildStubbed') do
  lookup ARGV.last
    search '(send nil create)'
    edit { |node| replace(node.loc.selector, 'build_stubbed') }
    policy { |new_file| system("bundle exec rspec --fail-fast #{new_file}") }
  end.run
end

RSpec: Use let_it_be instead of let

The let_it_be is a simple helper from TestProf gem that can speed up the specs by caching some factories using like a before_all approach.

This experiment hunts for let(...) { create(...) } and switch the let to let_it_be:

# Experimental replace `let(_)`  with `let_it_be` case it calls `create` inside the block
Fast.shortcut(:exp_let_it_be) do
  require 'fast/experiment'
  Fast.experiment('FactoryBot/LetItBe') do
    lookup ARGV.last
    search '(block (send nil let (sym _)) (args) (send nil create))'
    edit { |node| replace(node.children.first.loc.selector, 'let_it_be') }
    policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
  end.run
end

RSpec: Remove before or after blocks

From time to time, we forget some left overs like before or after blocks that even removing from the code, the tests still passes. This experiment removes the before/after blocks and check if the test passes.

# Experimental remove `before` or `after` blocks.
Fast.shortcut(:exp_remove_before_after) do
  require 'fast/experiment'
  Fast.experiment('RSpec/RemoveBeforeAfter') do
    lookup ARGV.last
    search '(block (send nil {before after}))'
    edit { |node| remove(node.loc.expression) }
    policy { |new_file| system("bin/spring rspec --fail-fast #{new_file}") }
  end.run
end

RSpec: Show message chains

I often forget the syntax and need to search for message chains on specs, so I created an shortcut for it.

# Show RSpec message chains
Fast.shortcut(:message_chains, '^^(send nil receive_message_chain)', 'spec')

RSpec: Show nested assertions

I love to use nested assertions and I often need examples to refer to them:

# Show RSpec nested assertions with .and
Fast.shortcut(:nested_assertions, '^^(send ... and)', 'spec')