{"id":13797994,"url":"https://github.com/jonatas/fast","last_synced_at":"2025-05-13T05:31:29.502Z","repository":{"id":43975009,"uuid":"91745728","full_name":"jonatas/fast","owner":"jonatas","description":"Find in AST - Search and refactor code directly in Abstract Syntax Tree as you do with grep for strings","archived":false,"fork":false,"pushed_at":"2024-08-27T15:13:55.000Z","size":2475,"stargazers_count":260,"open_issues_count":2,"forks_count":10,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-23T21:40:06.878Z","etag":null,"topics":["ast-representation","compiler","search-engine","syntax-tree","tree"],"latest_commit_sha":null,"homepage":"https://jonatas.github.io/fast/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jonatas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-05-18T23:20:25.000Z","updated_at":"2025-04-04T16:21:26.000Z","dependencies_parsed_at":"2023-11-09T02:23:32.348Z","dependency_job_id":"74b9c397-0a3b-44ff-ac62-4f7a57b988e0","html_url":"https://github.com/jonatas/fast","commit_stats":{"total_commits":238,"total_committers":9,"mean_commits":"26.444444444444443","dds":"0.10924369747899154","last_synced_commit":"746c3d6565b7651d995fc79ad1d353f40d1aabd8"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonatas%2Ffast","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonatas%2Ffast/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonatas%2Ffast/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonatas%2Ffast/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonatas","download_url":"https://codeload.github.com/jonatas/fast/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253883045,"owners_count":21978597,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast-representation","compiler","search-engine","syntax-tree","tree"],"created_at":"2024-08-04T00:00:37.575Z","updated_at":"2025-05-13T05:31:28.341Z","avatar_url":"https://github.com/jonatas.png","language":"Ruby","readme":"# Fast\n\n[![Build Status](https://travis-ci.org/jonatas/fast.svg?branch=master)](https://travis-ci.org/jonatas/fast)\n[![Maintainability](https://api.codeclimate.com/v1/badges/b03d62ee266399e76e32/maintainability)](https://codeclimate.com/github/jonatas/fast/maintainability)\n[![Test Coverage](https://api.codeclimate.com/v1/badges/b03d62ee266399e76e32/test_coverage)](https://codeclimate.com/github/jonatas/fast/test_coverage)\n\nFast, short for \"Find AST\", is a tool to search, prune, and edit Ruby ASTs.\n\nRuby is a flexible language that allows us to write code in multiple different ways\nto achieve the same end result, and because of this it's hard to verify how\nthe code was written without an AST.\n\nCheck out the official documentation: https://jonatas.github.io/fast.\n\n## Token Syntax for `find` in AST\n\nThe current version of Fast covers the following token elements:\n\n- `()` - represents a **node** search\n- `{}` - looks for **any** element to match, like a **Set** inclusion or `any?` in Ruby\n- `[]` - looks for **all** elements to match, like `all?` in Ruby.\n- `$` - will **capture** the contents of the current expression like a `Regex` group\n- `_` - represents any non-nil value, or **something** being present\n- `nil` -  matches exactly **nil**\n- `...` - matches a **node** with children\n- `^` - references the **parent node** of an expression\n- `?` - represents an element which **maybe** present\n- `\\1` - represents a substitution for any of the **previously captured** elements\n- `%1` - to bind the first extra argument in an expression\n- `\"\"` - will match a literal string with double quotes\n- `#\u003cmethod-name\u003e` - will call `\u003cmethod-name\u003e` with `node` as param allowing you\n    to build custom rules.\n- `.\u003cmethod-name\u003e` - will call `\u003cmethod-name\u003e` from the `node`\n\nThe syntax is inspired by the [RuboCop Node Pattern](https://github.com/rubocop-hq/rubocop-ast/blob/master/lib/rubocop/ast/node_pattern.rb).\n\n## Installation\n\n    $ gem install ffast\n\n## How it works\n\n### S-Expressions\n\nFast works by searching the abstract syntax tree using a series of expressions\nto represent code called `s-expressions`.\n\n\u003e `s-expressions`, or symbolic expressions, are a way to represent nested data.\n\u003e They originate from the LISP programming language, and are frequetly used in\n\u003e other languages to represent ASTs.\n\n### Integer Literals\n\nFor example, let's take an `Integer` in Ruby:\n\n```ruby\n1\n```\n\nIt's corresponding s-expression would be:\n\n```ruby\ns(:int, 1)\n```\n\n`s` in `Fast` and `Parser` are a shorthand for creating an `Parser::AST::Node`.\nEach of these nodes has a `#type` and `#children` contained in it:\n\n```ruby\ndef s(type, *children)\n  Parser::AST::Node.new(type, children)\nend\n```\n\n### Variable Assignments\n\nNow let's take a look at a local variable assignment:\n\n```ruby\nvalue = 42\n```\n\nIt's corresponding s-expression would be:\n\n```ruby\nast = s(:lvasgn, :value, s(:int, 42))\n```\n\nIf we wanted to find this particular assignment somewhere in our AST, we can use\nFast to look for a local variable named `value` with a value `42`:\n\n```ruby\nFast.match?('(lvasgn value (int 42))', ast) # =\u003e true\n```\n\n### Wildcard Token\n\nIf we wanted to find a variable named `value` that was assigned any integer value\nwe could replace `42` in our query with an underscore ( `_` ) as a shortcut:\n\n```ruby\nFast.match?('(lvasgn value (int _))', ast) # =\u003e true\n``` \n\n### Set Inclusion Token\n\nIf we weren't sure the type of the value we're assigning, we can use our set\ninclusion token (`{}`) from earlier to tell Fast that we expect either a `Float`\nor an `Integer`:\n \n```ruby\nFast.match?('(lvasgn value ({float int} _))', ast) # =\u003e true\n```\n\n### All Matching Token\n\nSay we wanted to say what we expect the value's type to _not_ be, we can use the\nall matching token (`[]`) to express multiple conditions that need to be true.\nIn this case we don't want the value to be a `String`, `Hash`, or an `Array` by\nprefixing all of the types with `!`:\n\n```ruby\nFast.match?('(lvasgn value ([!str !hash !array] _))', ast) # =\u003e true\n```\n\n### Node Child Token\n\nWe can match any node with children by using the child token ( `...` ):\n\n```ruby\nFast.match?('(lvasgn value ...)', ast) # =\u003e true\n```\n\nWe could even match any local variable assignment combining both `_` and `...`:\n\n```ruby\nFast.match?('(lvasgn _ ...)', ast) # =\u003e true\n```\n\n### Capturing the Value of an Expression\n\nYou can use `$` to capture the contents of an expression for later use:\n\n```ruby\nFast.match?('(lvasgn value $...)', ast) # =\u003e [s(:int, 42)]\n```\n\nCaptures can be used in any position as many times as you want to capture whatever\ninformation you might need:\n\n```ruby\nFast.match?('(lvasgn $_ $...)', ast) # =\u003e [:value, s(:int, 42)]\n```\n\n\u003e Keep in mind that `_` means something not nil and `...` means a node with\n\u003e children.\n\n### Calling Custom Methods\n\nYou can also define custom methods to set more complicated rules. Let's say\nwe're looking for duplicated methods in the same class. We need to collect\nmethod names and guarantee they are unique.\n\n```ruby\ndef duplicated(method_name)\n  @methods ||= []\n  already_exists = @methods.include?(method_name)\n  @methods \u003c\u003c method_name\n  already_exists\nend\n\nputs Fast.search_file('(def #duplicated)', 'example.rb')\n```\n\nThe same principle can be used in the node level or for debugging purposes.\n\n```ruby\n    require 'pry'\n    def debug(node)\n      binding.pry\n    end\n\n    puts Fast.search_file('#debug', 'example.rb')\n```\nIf you want to get only `def` nodes you can also intersect expressions with `[]`:\n\n```ruby\nputs Fast.search_file('[ def #debug ]', 'example.rb')\n```\n\n### Methods\n\nLet's take a look at a method declaration:\n\n```ruby\ndef my_method\n  call_other_method\nend\n```\n\nIt's corresponding s-expression would be:\n\n```ruby\nast =\n  s(:def, :my_method,\n    s(:args),\n    s(:send, nil, :call_other_method))\n```\n\nNote the node `(args)`. We can't use `...` to match it, as it\nhas no children (or arguments in this case), but we _can_ match it with a wildcard\n`_` as it's not `nil`.\n\n### Call Chains\n\nLet's take a look at a few other examples. Sometimes you have a chain of calls on\na single `Object`, like `a.b.c.d`. Its corresponding s-expression would be:\n\n```ruby\nast =\n  s(:send,\n    s(:send,\n      s(:send,\n        s(:send, nil, :a),\n        :b),\n      :c),\n    :d)\n```\n\n### Alternate Syntax\n\nYou can also search using nested arrays with **pure values**, or **shortcuts** or\n**procs**:\n\n```ruby\nFast.match? [:send, [:send, '...'], :d], ast  # =\u003e true\nFast.match? [:send, [:send, '...'], :c], ast  # =\u003e false\n```\n\nShortcut tokens like child nodes `...` and wildcards `_` are just placeholders\nfor procs. If you want, you can even use procs directly like so:\n\n```ruby\nFast.match?([\n  :send, [\n    -\u003e (node) { node.type == :send },\n    [:send, '...'],\n    :c\n  ],\n  :d\n], ast) # =\u003e true\n```\n\nThis also works with expressions:\n\n```ruby\nFast.match?('(send (send (send (send nil $_) $_) $_) $_)', ast) # =\u003e [:a, :b, :c, :d]\n```\n\n### Debugging\n\nIf you find that a particular expression isn't working, you can use `debug` to\ntake a look at what Fast is doing:\n\n```ruby\nFast.debug { Fast.match?([:int, 1], s(:int, 1)) }\n```\n\nEach comparison made while searching will be logged to your console (STDOUT) as\nFast goes through the AST:\n\n    int == (int 1) # =\u003e true\n    1 == 1 # =\u003e true\n\n## Bind arguments to expressions\n\nWe can also dynamically interpolate arguments into our queries using the\ninterpolation token `%`. This works much like `sprintf` using indexes starting\nfrom `1`:\n\n```ruby\nFast.match? '(lvasgn %1 (int _))', ('a = 1'), :a  # =\u003e true\n```\n\n## Using previous captures in search\n\nImagine you're looking for a method that is just delegating something to\nanother method, like this `name` method:\n\n```ruby\ndef name\n  person.name\nend\n```\n\nThis can be represented as the following AST:\n\n```\n(def :name\n  (args)\n  (send\n    (send nil :person) :name))\n```\n\nWe can create a query that searches for such a method:\n\n```ruby\nFast.match?('(def $_ ... (send (send nil _) \\1))', ast) # =\u003e [:name]\n```\n    \n\n## Fast.search\n\nSearch allows you to go search the entire AST, collecting nodes that matches given\nexpression. Any matching node is then returned:\n\n```ruby\nFast.search('(int _)', Fast.ast('a = 1')) # =\u003e s(:int, 1)\n```\n\nIf you use captures along with a search, both the matching nodes and the\ncaptures will be returned:\n\n```ruby\nFast.search('(int $_)', Fast.ast('a = 1')) # =\u003e [s(:int, 1), 1]\n```\n    \n\nYou can also bind external parameters from the search:\n\n```ruby\nFast.search('(int %1)', Fast.ast('a = 1'), 1) # =\u003e [s(:int, 1)]\n```\n\n## Fast.capture\n\nTo only pick captures and ignore the nodes, use `Fast.capture`:\n\n```ruby\nFast.capture('(int $_)', Fast.ast('a = 1')) # =\u003e 1\n```\n\n## Fast.replace\n\nLet's consider the following example:\n\n```ruby\ndef name\n  person.name\nend\n```\n\nAnd, we want to replace code to use `delegate` in the expression:\n\n```ruby\ndelegate :name, to: :person\n```\n\nWe already target this example using `\\1` on \n[Search and refer to previous capture](#using-previous-captures-in-search) and\nnow it's time to know about how to rewrite content.\n\nThe [Fast.replace](Fast#replace-class_method) yields a #{Fast::Rewriter} context.\nThe internal replace method accepts a range and every `node` have\na `location` with metadata about ranges of the node expression.\n\n```ruby\nast = Fast.ast(\"def name; person.name end\")\n# =\u003e s(:def, :name, s(:args), s(:send, s(:send, nil, :person), :name))\n```\n\nGenerally, we  use the `location.expression`:\n\n```ruby\nast.location.expression # =\u003e #\u003cParser::Source::Range (string) 0...25\u003e\n```\n\nBut location also brings some metadata about specific fragments:\n\n```ruby\nast.location.instance_variables # =\u003e [:@keyword, :@operator, :@name, :@end, :@expression, :@node]\n```\n\nRange for the keyword that identifies the method definition:\n```ruby\nast.location.keyword # =\u003e #\u003cParser::Source::Range (string) 0...3\u003e\n```\n\nYou can always pick the source of a source range:\n\n```ruby\nast.location.keyword.source # =\u003e \"def\"\n```\n\nOr only the method name:\n\n```ruby\nast.location.name # =\u003e #\u003cParser::Source::Range (string) 4...8\u003e\nast.location.name.source # =\u003e \"name\"\n```\n\nIn the context of the rewriter, the objective is removing the method and inserting the new\ndelegate content. Then, the scope is `node.location.expression`:\n\n```ruby\nFast.replace '(def $_ ... (send (send nil $_) \\1))', ast do |node, captures|\n  attribute, object = captures\n\n  replace(\n    node.location.expression,\n    \"delegate :#{attribute}, to: :#{object}\"\n  )\nend\n```\n\n### Replacing file\n\nNow let's imagine we have a file like `sample.rb` with the following code:\n\n```ruby\ndef good_bye\n  message = [\"good\", \"bye\"]\n  puts message.join(' ')\nend\n```\n\nand we decide to inline the contents of the `message` variable right after\n\n```ruby\ndef good_bye\n  puts [\"good\", \"bye\"].join(' ')\nend\n```\n\nTo refactor and reach the proposed example, follow a few steps:\n\n1. Remove the local variable assignment\n2. Store the now-removed variable's value\n3. Substitute the value where the variable was used before\n\n#### Entire example\n\n```ruby\nassignment = nil\nFast.replace_file '({ lvasgn lvar } message )', 'sample.rb' do |node, _|\n  # Find a variable assignment\n  if node.type == :lvasgn\n    assignment = node.children.last\n    # Remove the node responsible for the assignment\n    remove(node.location.expression)\n  # Look for the variable being used\n  elsif node.type == :lvar\n    # Replace the variable with the contents of the variable\n    replace(\n      node.location.expression,\n      assignment.location.expression.source\n    )\n  end\nend \n```\n\nKeep in mind the current example returns a content output but do not rewrite the\nfile.\n\n## Other utility functions\n\nTo manipulate ruby files, sometimes you'll need some extra tasks.\n\n## Fast.ast_from_file(file)\n\nThis method parses code from a file and loads it into an AST representation.\n```ruby\nFast.ast_from_file('sample.rb')\n```\n\n## Fast.search_file\n\nYou can use `search_file` to for search for expressions inside files.\n\n```ruby\nFast.search_file(expression, 'file.rb')\n```\n\nIt's a combination of `Fast.ast_from_file` with `Fast.search`.\n\n## Fast.capture_file\n\nYou can use `Fast.capture_file` to only return captures:\n\n```ruby\nFast.capture_file('(class (const nil $_))', 'lib/fast.rb')\n# =\u003e [:Rewriter, :ExpressionParser, :Find, :FindString, ...]\n```\n\n## Fast.ruby_files_from(arguments)\n\nThe `Fast.ruby_files_from(arguments)` can get all ruby files from file list or folders:\n\n```ruby\nFast.ruby_files_from('lib') \n# =\u003e [\"lib/fast/experiment.rb\", \"lib/fast/cli.rb\", \"lib/fast/version.rb\", \"lib/fast.rb\"]\n```\n\n\u003e Note: it doesn't support glob special selectors like `*.rb` or `**/*` as it\n\u003e recursively looks for ruby files in the givem params.\n\n## `fast` in the command line\n\nFast also comes with a command line utility called `fast`. You can use it to\nsearch and find code much like the library version:\n\n    fast '(def match?)' lib/fast.rb\n\nThe CLI tool takes the following flags\n\n- Use `-d` or `--debug` for enable debug mode.\n- Use `--ast` to output the AST instead of the original code\n- Use `--pry` to jump debugging the first result with pry\n- Use `-c` to search from code example\n- Use `-s` to search similar code\n- Use `-p` or `--parallel` to parallelize the search\n\n### Define your `Fastfile`\n\nFastfile is loaded when you start a pattern with a `.`.\n\nYou can also define extra Fastfile in your home dir or setting a directory with\nthe `FAST_FILE_DIR`.\n\nYou can define a `Fastfile` in any project with your custom shortcuts.\n\n```ruby\nFast.shortcut(:version, '(casgn nil VERSION (str _))', 'lib/fast/version.rb')\n```\n\nLet's say you'd like to show the version of your library. Your normal\ncommand line will look like:\n\n    $ fast '(casgn nil VERSION)' lib/*/version.rb\n\nOr generalizing to search all constants in the version files:\n\n    $ fast casgn lib/*/version.rb\n\nIt will output but the command is not very handy. In order to just say `fast .version`\nyou can use the previous snipped in your `Fastfile`.\n\nAnd it will output something like this:\n\n```ruby\n# lib/fast/version.rb:4\nVERSION = '0.1.2'\n```\n\nCreate shortcuts with blocks that are able to introduce custom coding in\nthe scope of the `Fast` module\n\nTo bump a new version of your library for example you can type `fast .bump_version`\nand add the snippet to your library fixing the filename.\n\n```ruby\nFast.shortcut :bump_version do\n  rewrite_file('(casgn nil VERSION (str _)', 'lib/fast/version.rb') do |node|\n    target = node.children.last.loc.expression\n    pieces = target.source.split(\".\").map(\u0026:to_i)\n    pieces.reverse.each_with_index do |fragment,i|\n      if fragment \u003c 9\n        pieces[-(i+1)] = fragment +1\n        break\n      else\n        pieces[-(i+1)] = 0\n      end\n    end\n    replace(target, \"'#{pieces.join(\".\")}'\")\n  end\nend\n```\n\nYou can find more examples in the [Fastfile](./Fastfile).\n\n### Fast with Pry\n\nYou can use `--pry` to stop on a particular source node, and run Pry at that\nlocation:\n\n    fast '(block (send nil it))' spec --pry\n\nInside the pry session you can access `result` for the first result that was\nlocated, or `results` to get all of the occurrences found.\n\nLet's take a look at `results`:\n\n    results.map { |e| e.children[0].children[2] }\n    # =\u003e [s(:str, \"parses ... as Find\"),\n    # s(:str, \"parses $ as Capture\"),\n    # s(:str, \"parses quoted values as strings\"),\n    # s(:str, \"parses {} as Any\"),\n    # s(:str, \"parses [] as All\"), ...]\n\n### Fast with RSpec\n\nLet's say we wanted to get all the `it` blocks in our `RSpec` code that\ncurrently do not have descriptions:\n\n    fast '(block (send nil it (nil)) (args) (!str)) ) )' spec\n\nThis will return the following:\n\n    # spec/fast_spec.rb:166\n    it { expect(described_class).to be_match(s(:int, 1), '(...)') }\n    # spec/fast_spec.rb:167\n    it { expect(described_class).to be_match(s(:int, 1), '(_ _)') }\n    # spec/fast_spec.rb:168\n    it { expect(described_class).to be_match(code['\"string\"'], '(str \"string\")') }\n\n## Experiments\n\nExperiments can be used to run experiments against your code in an automated\nfashion. These experiments can be used to test the effectiveness of things\nlike performance enhancements, or if a replacement piece of code actually works\nor not.\n\nLet's create an experiment to try and remove all `before` and `after` blocks\nfrom our specs.\n\nIf the spec still pass we can confidently say that the hook is useless.\n\n```ruby\nFast.experiment(\"RSpec/RemoveUselessBeforeAfterHook\") do\n  # Lookup our spec files\n  lookup 'spec'\n\n  # Look for every block starting with before or after\n  search \"(block (send nil {before after}))\"\n\n  # Remove those blocks\n  edit { |node| remove(node.loc.expression) }\n\n  # Create a new file, and run RSpec against that new file\n  policy { |new_file| system(\"bin/spring rspec --fail-fast #{new_file}\") }\nend\n```\n\n- `lookup` can be used to pass in files or folders.\n- `search` contains the expression you want to match\n- `edit` is used to apply code change\n- `policy` is what we execute to verify the current change still passes\n\nEach removal of a `before` and `after` block will occur in isolation to verify\neach one of them independently of the others. Each successful removal will be\nkept in a secondary change until we run out of blocks to remove.\n\nYou can see more examples in the [experiments](experiments) folder.\n\n### Running Multiple Experiments\n\nTo run multiple experiments, use `fast-experiment` runner:\n\n```\nfast-experiment \u003cexperiment-names\u003e \u003cfiles-or-folders\u003e\n```\n\nYou can limit the scope of experiments:\n\n```\nfast-experiment RSpec/RemoveUselessBeforeAfterHook spec/models/**/*_spec.rb\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nOn the console we have a few functions like `s` and `code` to make it easy ;)\n\n    bin/console\n\n```ruby\ncode(\"a = 1\") # =\u003e s(:lvasgn, s(:int, 1))\n```\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/jonatas/fast. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\nSee more on the [official documentation](https://jonatas.github.io/fast).\n","funding_links":[],"categories":["Ruby","Gems"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonatas%2Ffast","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonatas%2Ffast","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonatas%2Ffast/lists"}