jonatas/fast

View on GitHub
docs/shortcuts.md

Summary

Maintainability
Test Coverage
# Shortcuts

Shortcuts are defined on a `Fastfile` inside any ruby project.

!!!info "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`:

```ruby
# 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:


```ruby
# 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:

```ruby
# 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
```

!!! info "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](https://github.com/jonatas/fast/blob/master/Fastfile#L20-L34)
to bump a new version of the gem.

```ruby
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`:

```diff
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.

```ruby
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:

```ruby
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:


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

Wrapping up everything in a shortcut:

```ruby
# 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
```

!!! faq "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

!!! hint "First shortcut with experiments"
    If you're not familiar with automated experiments, you can read about it [here](/experiments).

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:

```ruby
# 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.

```ruby
# 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](https://test-prof.evilmartians.io/#/let_it_be) 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`:

```ruby
# 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.

```ruby
# 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.

```ruby
# 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:

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