ffast 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agents/fast-pattern-expert/SKILL.md +71 -0
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +2 -0
- data/Fastfile +105 -18
- data/README.md +21 -7
- data/bin/console +1 -1
- data/bin/fast-experiment +3 -0
- data/bin/fast-mcp +7 -0
- data/fast.gemspec +1 -3
- data/ideia_blog_post.md +36 -0
- data/lib/fast/cli.rb +74 -23
- data/lib/fast/experiment.rb +19 -2
- data/lib/fast/git.rb +1 -1
- data/lib/fast/mcp_server.rb +341 -0
- data/lib/fast/node.rb +258 -0
- data/lib/fast/prism_adapter.rb +327 -0
- data/lib/fast/rewriter.rb +64 -10
- data/lib/fast/scan.rb +207 -0
- data/lib/fast/shortcut.rb +16 -4
- data/lib/fast/source.rb +116 -0
- data/lib/fast/source_rewriter.rb +153 -0
- data/lib/fast/sql/rewriter.rb +36 -7
- data/lib/fast/sql.rb +15 -17
- data/lib/fast/summary.rb +440 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +218 -101
- data/mkdocs.yml +19 -4
- data/requirements-docs.txt +3 -0
- metadata +18 -59
- data/docs/command_line.md +0 -238
- data/docs/editors-integration.md +0 -46
- data/docs/experiments.md +0 -155
- data/docs/git.md +0 -115
- data/docs/ideas.md +0 -70
- data/docs/index.md +0 -404
- data/docs/pry-integration.md +0 -27
- data/docs/research.md +0 -93
- data/docs/shortcuts.md +0 -323
- data/docs/similarity_tutorial.md +0 -176
- data/docs/sql-support.md +0 -253
- data/docs/syntax.md +0 -395
- data/docs/videos.md +0 -16
- data/docs/walkthrough.md +0 -135
- data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
- data/examples/experimental_replacement.rb +0 -46
- data/examples/find_usage.rb +0 -26
- data/examples/let_it_be_experiment.rb +0 -11
- data/examples/method_complexity.rb +0 -37
- data/examples/search_duplicated.rb +0 -15
- data/examples/similarity_research.rb +0 -58
- data/examples/simple_rewriter.rb +0 -6
- data/experiments/let_it_be_experiment.rb +0 -9
- data/experiments/remove_useless_hook.rb +0 -9
- data/experiments/replace_create_with_build_stubbed.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b5b5e27292bfe589236ccf40952b5da73f4dfc4b35df85220d9beebae7c725e
|
|
4
|
+
data.tar.gz: 6c4466b554e164c447cfb02760c5b6cd262c0f904f7ea8c35897288efa4668ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f8ccd9bcd621b69847fd1092b0fa08cada32258a2ec1f6b04654744c8930718628955698673577e4431a6fe6146da744bcb6aafdada442aa08bfe8e5ba32780
|
|
7
|
+
data.tar.gz: 263b032df9ed1ea5b8d7cd71e1bd613dc781c53fc1dd10cb8bedbe526d904543e1331bb43f345679954e7ebc404b0bdfb5f965dffd35b74c454038ba7ae0fb31
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Skill: Fast Pattern Expert
|
|
2
|
+
|
|
3
|
+
You are an expert in constructing and translating natural language queries into `Fast` AST patterns for Ruby.
|
|
4
|
+
|
|
5
|
+
## Core Expertise
|
|
6
|
+
- Translating structural descriptions of Ruby code into S-expression based AST patterns.
|
|
7
|
+
- Understanding the Ruby AST (Prism/Parser based).
|
|
8
|
+
- Navigating and searching codebases semantically using `Fast`.
|
|
9
|
+
|
|
10
|
+
## Syntax Reference
|
|
11
|
+
- `(node_type ...)` : Search for a node of a specific type.
|
|
12
|
+
- `_` : Matches any non-nil node/value.
|
|
13
|
+
- `nil` : Matches exactly `nil`.
|
|
14
|
+
- `...` : When last in a list, matches zero or more remaining children. Elsewhere, matches a node with children.
|
|
15
|
+
- `^` : Navigate to the parent node.
|
|
16
|
+
- `$` : Capture the matched node or value.
|
|
17
|
+
- `{type1 type2}` : Union (OR) - matches if any internal expression matches.
|
|
18
|
+
- `[expr1 expr2]` : Intersection (AND) - matches only if all internal expressions match.
|
|
19
|
+
- `!expr` : Negation (NOT) - matches if the expression does not match.
|
|
20
|
+
- `?expr` : Maybe - matches if the node is nil or matches the expression.
|
|
21
|
+
- `\1`, `\2` : Backreference to previous captures.
|
|
22
|
+
- `#custom_method` : Call a custom Ruby method for validation.
|
|
23
|
+
- `.instance_method?` : Call an instance method on the node for validation (e.g., `.odd?`).
|
|
24
|
+
|
|
25
|
+
## Common Ruby AST Nodes
|
|
26
|
+
- `(def name (args) body)` : Method definition.
|
|
27
|
+
- `(defs receiver name (args) body)` : Singleton method definition.
|
|
28
|
+
- `(send receiver method_name args...)` : Method call.
|
|
29
|
+
- `(class name superclass body)` : Class definition.
|
|
30
|
+
- `(module name body)` : Module definition.
|
|
31
|
+
- `(const scope name)` : Constant reference (scope is nil for top-level).
|
|
32
|
+
- `(casgn scope name value)` : Constant assignment.
|
|
33
|
+
- `(lvar name)` : Local variable read.
|
|
34
|
+
- `(lvasgn name value)` : Local variable assignment.
|
|
35
|
+
- `(ivar name)` : Instance variable read.
|
|
36
|
+
- `(ivasgn name value)` : Instance variable assignment.
|
|
37
|
+
- `(hash (pair key value)...)` : Hash literal.
|
|
38
|
+
- `(array elements...)` : Array literal.
|
|
39
|
+
|
|
40
|
+
## Translation Examples
|
|
41
|
+
|
|
42
|
+
### Methods
|
|
43
|
+
- "Find all methods named 'process'" -> `(def process)`
|
|
44
|
+
- "Find methods with at least 3 arguments" -> `(def _ (args _ _ _ ...))`
|
|
45
|
+
- "Find singleton methods (self.method)" -> `(defs ...)`
|
|
46
|
+
- "Find methods that call 'super'" -> `(def _ _ (send nil :super ...))`
|
|
47
|
+
|
|
48
|
+
### Classes & Modules
|
|
49
|
+
- "Find classes inheriting from ApplicationController" -> `(class _ (const nil ApplicationController))`
|
|
50
|
+
- "Find classes defined inside the 'User' namespace" -> `(class (const (const nil User) _) ...)`
|
|
51
|
+
- "Find modules that include 'Enumerable'" -> `(module _ (begin < (send nil include (const nil Enumerable)) ...))`
|
|
52
|
+
|
|
53
|
+
### Method Calls
|
|
54
|
+
- "Find all calls to 'User.find'" -> `(send (const nil User) find ...)`
|
|
55
|
+
- "Find calls to 'where' with a hash argument" -> `(send _ where (hash ...))`
|
|
56
|
+
- "Find calls to 'exit' or 'abort'" -> `(send nil {exit abort} ...)`
|
|
57
|
+
|
|
58
|
+
### Variables & Constants
|
|
59
|
+
- "Find where the 'DEBUG' constant is assigned" -> `(casgn nil DEBUG)`
|
|
60
|
+
- "Find all uses of instance variable '@user'" -> `(ivar @user)`
|
|
61
|
+
- "Find assignments to '@user'" -> `(ivasgn @user)`
|
|
62
|
+
|
|
63
|
+
## Strategy for Complex Queries
|
|
64
|
+
1. **Identify the anchor node**: What is the primary structure? (e.g., a method definition, a specific call).
|
|
65
|
+
2. **Describe children**: What must be true about its arguments or body?
|
|
66
|
+
3. **Use Union/Intersection**: Combine multiple constraints using `{}` or `[]`.
|
|
67
|
+
4. **Capture if needed**: Use `$` if you only want a specific part of the match.
|
|
68
|
+
5. **Validate**: Always use `validate_fast_pattern` if available to check syntax.
|
|
69
|
+
|
|
70
|
+
## AST Triage
|
|
71
|
+
If you are unsure of the AST structure for a piece of code, use `Fast.ast("your code snippet")` or `Fast.ast_from_file` to see the s-expression representation. This is the most reliable way to build a pattern.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*' # Triggers when a new tag like v0.2.0 is pushed
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
name: Build and Release
|
|
11
|
+
runs-on: ubuntu-22.04
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read # Required to checkout the code
|
|
14
|
+
id-token: write # Required for RubyGems Trusted Publishing
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: '3.3'
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Publish to RubyGems
|
|
27
|
+
uses: rubygems/release-gem@v1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
2
|
+
# They are provided by a third-party and are governed by
|
|
3
|
+
# separate terms of service, privacy policy, and support
|
|
4
|
+
# documentation.
|
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
|
7
|
+
|
|
8
|
+
name: Ruby
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [ "master" ]
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [ "master" ]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-22.04
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
ruby-version: ['3.0', '3.1', '3.2', '3.3']
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Set up Ruby
|
|
29
|
+
uses: ruby/setup-ruby@v1
|
|
30
|
+
with:
|
|
31
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
32
|
+
bundler-cache: true
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/Fastfile
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
begin
|
|
3
|
+
require 'fast/source'
|
|
4
|
+
rescue LoadError
|
|
5
|
+
nil
|
|
6
|
+
end
|
|
7
|
+
|
|
2
8
|
# Fastfile is loaded when you start an expression with a dot.
|
|
3
9
|
#
|
|
4
10
|
# You can introduce shortcuts or methods that can be embedded during your
|
|
@@ -9,7 +15,7 @@ version_file = Dir['lib/*/version.rb'].first
|
|
|
9
15
|
Fast.shortcut(:version, '(casgn nil VERSION (str _))', version_file)
|
|
10
16
|
|
|
11
17
|
# Show all classes that inherits Fast::Find
|
|
12
|
-
Fast.shortcut(:finders, '(class ... (const nil Find)', 'lib')
|
|
18
|
+
Fast.shortcut(:finders, '(class ... (const nil Find))', 'lib')
|
|
13
19
|
|
|
14
20
|
# You can run shortcuts appending a dot to the shortcut.
|
|
15
21
|
# $ fast .version
|
|
@@ -17,11 +23,13 @@ Fast.shortcut(:finders, '(class ... (const nil Find)', 'lib')
|
|
|
17
23
|
# VERSION = '0.1.2'
|
|
18
24
|
|
|
19
25
|
# Simple shortcut that I used often to show how the expression parser works
|
|
20
|
-
Fast.shortcut(:parser, '(class (const nil ExpressionParser)', 'lib/fast.rb')
|
|
26
|
+
Fast.shortcut(:parser, '(class (const nil ExpressionParser))', 'lib/fast.rb')
|
|
27
|
+
Fast.shortcut(:sql_parser, '(def parse ...)', 'lib/fast/sql.rb')
|
|
21
28
|
|
|
22
29
|
# Use `fast .bump_version` to rewrite the version file
|
|
23
30
|
Fast.shortcut :bump_version do
|
|
24
|
-
|
|
31
|
+
new_version = nil
|
|
32
|
+
rewrite_file('(casgn nil VERSION (str _))', version_file) do |node|
|
|
25
33
|
target = node.children.last.loc.expression
|
|
26
34
|
pieces = target.source.split('.').map(&:to_i)
|
|
27
35
|
pieces.reverse.each_with_index do |fragment, i|
|
|
@@ -32,7 +40,24 @@ Fast.shortcut :bump_version do
|
|
|
32
40
|
pieces[-(i + 1)] = 0
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
|
-
|
|
43
|
+
new_version = pieces.join('.')
|
|
44
|
+
replace(target, "'#{new_version}'")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
print "Commit bumped version v#{new_version}? (y/n) "
|
|
48
|
+
if $stdin.gets.chomp.downcase == 'y'
|
|
49
|
+
system("git add #{version_file} && git commit -m 'Bump version to v#{new_version}'")
|
|
50
|
+
|
|
51
|
+
print "Tag version v#{new_version}? (y/n) "
|
|
52
|
+
if $stdin.gets.chomp.downcase == 'y'
|
|
53
|
+
system("git tag v#{new_version}")
|
|
54
|
+
|
|
55
|
+
print "Push commit and tag to origin? (y/n) "
|
|
56
|
+
if $stdin.gets.chomp.downcase == 'y'
|
|
57
|
+
system("git push origin HEAD")
|
|
58
|
+
system("git push origin v#{new_version}")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
36
61
|
end
|
|
37
62
|
end
|
|
38
63
|
|
|
@@ -57,22 +82,9 @@ Fast.shortcut :intro do
|
|
|
57
82
|
Fast.shortcuts[:walk].run
|
|
58
83
|
end
|
|
59
84
|
|
|
60
|
-
# Useful for `fast .walk file.md` but not required by the library.
|
|
61
|
-
private
|
|
62
|
-
def require_or_install_tty_md
|
|
63
|
-
require 'tty-markdown'
|
|
64
|
-
rescue LoadError
|
|
65
|
-
puts 'Installing tty-markdown gem to better engage you :)'
|
|
66
|
-
Gem.install('tty-markdown')
|
|
67
|
-
puts 'Done! Now, back to our topic \o/'
|
|
68
|
-
system('clear')
|
|
69
|
-
retry
|
|
70
|
-
end
|
|
71
|
-
|
|
72
85
|
# Interactive command line walkthrough
|
|
73
86
|
# fast .walk docs/walkthrough.md
|
|
74
87
|
Fast.shortcut :walk do
|
|
75
|
-
require_or_install_tty_md
|
|
76
88
|
file = ARGV.last
|
|
77
89
|
execute = ->(line) { system(line) }
|
|
78
90
|
walk = ->(line) { line.each_char { |c| sleep(0.02) and print(c) } }
|
|
@@ -87,7 +99,82 @@ Fast.shortcut :walk do
|
|
|
87
99
|
when /^!{3}\s/
|
|
88
100
|
# Skip warnings that are only for web tutorials
|
|
89
101
|
else
|
|
90
|
-
walk[
|
|
102
|
+
walk[Fast.render_markdown_for_terminal(line)]
|
|
91
103
|
end
|
|
92
104
|
end
|
|
93
105
|
end
|
|
106
|
+
|
|
107
|
+
# Format SQL
|
|
108
|
+
Fast.shortcut :format_sql do
|
|
109
|
+
require 'fast/sql'
|
|
110
|
+
file = ARGV.last
|
|
111
|
+
method = File.exist?(file) ? :parse_sql_file : :parse_sql
|
|
112
|
+
ast = Fast.public_send(method, file)
|
|
113
|
+
ast = ast.first if ast.is_a? Array
|
|
114
|
+
eligible_kw = [:RESERVED_KEYWORD]
|
|
115
|
+
eligible_tokens = [:BY]
|
|
116
|
+
|
|
117
|
+
output = Fast::SQL.replace('_', ast) do |root|
|
|
118
|
+
sb = root.loc.expression.source_buffer
|
|
119
|
+
sb.tokens.each do |token|
|
|
120
|
+
if eligible_kw.include?(token.keyword_kind) || eligible_tokens.include?(token.token)
|
|
121
|
+
range = Fast::Source.range(sb, token.start, token.end)
|
|
122
|
+
replace(range, range.source.upcase)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
require 'fast/cli'
|
|
127
|
+
puts Fast.highlight(output, sql: true)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Anonymize SQL
|
|
131
|
+
# fast .anonymize_sql file.sql
|
|
132
|
+
Fast.shortcut :anonymize_sql do
|
|
133
|
+
require 'fast/sql'
|
|
134
|
+
file = ARGV.last
|
|
135
|
+
method = File.exist?(file) ? :parse_sql_file : :parse_sql
|
|
136
|
+
ast = Fast.public_send(method, file)
|
|
137
|
+
memo = {}
|
|
138
|
+
|
|
139
|
+
relnames = search("(relname $_)", ast).grep(String).uniq
|
|
140
|
+
pattern = "{relname (sval {#{relnames.map(&:inspect).join(' ')}})}"
|
|
141
|
+
puts "searching with #{pattern}"
|
|
142
|
+
|
|
143
|
+
content = Fast::SQL.replace(pattern, ast) do |node|
|
|
144
|
+
new_name = memo[node.source.tr(%|"'|, '')] ||= "x#{memo.size}"
|
|
145
|
+
new_name = "'#{new_name}'" if node.type == :sval
|
|
146
|
+
replace(node.loc.expression, new_name)
|
|
147
|
+
end
|
|
148
|
+
puts Fast.highlight(content, sql: true)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Give all details in a shorter format
|
|
152
|
+
# fast .summary file.rb
|
|
153
|
+
Fast.shortcut :summary do
|
|
154
|
+
file = ARGV.reverse.find { |arg| !arg.start_with?('-') && File.exist?(arg) }
|
|
155
|
+
if file && File.exist?(file)
|
|
156
|
+
Fast.summary(IO.read(file), file: file, level: fast_option_value(ARGV, '-l', '--level')).summarize
|
|
157
|
+
else
|
|
158
|
+
puts "Please provide a valid file to summarize."
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Group and classify multiple Ruby files without printing full bodies
|
|
163
|
+
# fast .scan lib app/models --no-color
|
|
164
|
+
Fast.shortcut :scan do
|
|
165
|
+
locations = ARGV.select { |arg| !arg.start_with?('-') && File.exist?(arg) }
|
|
166
|
+
if locations.any?
|
|
167
|
+
Fast.scan(locations, level: fast_option_value(ARGV, '-l', '--level')).scan
|
|
168
|
+
else
|
|
169
|
+
puts "Please provide at least one valid file or directory to scan."
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def fast_option_value(args, short_name, long_name)
|
|
174
|
+
args.each_with_index do |arg, index|
|
|
175
|
+
return args[index + 1] if arg == short_name || arg == long_name
|
|
176
|
+
return arg.split('=', 2).last if arg.start_with?("#{long_name}=")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
nil
|
|
180
|
+
end
|
data/README.md
CHANGED
|
@@ -57,18 +57,18 @@ For example, let's take an `Integer` in Ruby:
|
|
|
57
57
|
1
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Its corresponding s-expression would be:
|
|
61
61
|
|
|
62
62
|
```ruby
|
|
63
63
|
s(:int, 1)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
`s` in `Fast`
|
|
66
|
+
`s` in `Fast` is a shorthand for creating a `Fast::Node`.
|
|
67
67
|
Each of these nodes has a `#type` and `#children` contained in it:
|
|
68
68
|
|
|
69
69
|
```ruby
|
|
70
70
|
def s(type, *children)
|
|
71
|
-
|
|
71
|
+
Fast::Node.new(type, children: children)
|
|
72
72
|
end
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -198,7 +198,7 @@ def my_method
|
|
|
198
198
|
end
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
Its corresponding s-expression would be:
|
|
202
202
|
|
|
203
203
|
```ruby
|
|
204
204
|
ast =
|
|
@@ -372,7 +372,7 @@ ast = Fast.ast("def name; person.name end")
|
|
|
372
372
|
Generally, we use the `location.expression`:
|
|
373
373
|
|
|
374
374
|
```ruby
|
|
375
|
-
ast.location.expression # => #<
|
|
375
|
+
ast.location.expression # => #<Fast::Source::Range (string) 0...25>
|
|
376
376
|
```
|
|
377
377
|
|
|
378
378
|
But location also brings some metadata about specific fragments:
|
|
@@ -383,7 +383,7 @@ ast.location.instance_variables # => [:@keyword, :@operator, :@name, :@end, :@ex
|
|
|
383
383
|
|
|
384
384
|
Range for the keyword that identifies the method definition:
|
|
385
385
|
```ruby
|
|
386
|
-
ast.location.keyword # => #<
|
|
386
|
+
ast.location.keyword # => #<Fast::Source::Range (string) 0...3>
|
|
387
387
|
```
|
|
388
388
|
|
|
389
389
|
You can always pick the source of a source range:
|
|
@@ -395,7 +395,7 @@ ast.location.keyword.source # => "def"
|
|
|
395
395
|
Or only the method name:
|
|
396
396
|
|
|
397
397
|
```ruby
|
|
398
|
-
ast.location.name # => #<
|
|
398
|
+
ast.location.name # => #<Fast::Source::Range (string) 4...8>
|
|
399
399
|
ast.location.name.source # => "name"
|
|
400
400
|
```
|
|
401
401
|
|
|
@@ -680,6 +680,20 @@ code("a = 1") # => s(:lvasgn, s(:int, 1))
|
|
|
680
680
|
|
|
681
681
|
To 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).
|
|
682
682
|
|
|
683
|
+
## Documentation locally
|
|
684
|
+
|
|
685
|
+
The documentation site is built with MkDocs Material and a few Markdown
|
|
686
|
+
extensions. To run it locally:
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
python3 -m venv .venv
|
|
690
|
+
source .venv/bin/activate
|
|
691
|
+
python3 -m pip install -r requirements-docs.txt
|
|
692
|
+
mkdocs serve
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Then open `http://127.0.0.1:8000`.
|
|
696
|
+
|
|
683
697
|
## Contributing
|
|
684
698
|
|
|
685
699
|
Bug 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.
|
data/bin/console
CHANGED
data/bin/fast-experiment
CHANGED
|
@@ -8,6 +8,7 @@ require 'fast/experiment'
|
|
|
8
8
|
require 'coderay'
|
|
9
9
|
|
|
10
10
|
arguments = ARGV
|
|
11
|
+
autoclean = arguments.delete('--autoclean')
|
|
11
12
|
|
|
12
13
|
experiment_files = Fast.ruby_files_from(File.expand_path('../experiments', __dir__))
|
|
13
14
|
experiment_files.each(&method(:require))
|
|
@@ -23,6 +24,7 @@ end
|
|
|
23
24
|
if arguments.any?
|
|
24
25
|
ruby_files = arguments.all? { |e| File.exist?(e) && e.end_with?('.rb') }
|
|
25
26
|
experiments.each do |experiment|
|
|
27
|
+
experiment.autoclean = autoclean
|
|
26
28
|
if ruby_files
|
|
27
29
|
experiment.files = arguments
|
|
28
30
|
else
|
|
@@ -30,4 +32,5 @@ if arguments.any?
|
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
35
|
+
experiments.each { |experiment| experiment.autoclean = autoclean }
|
|
33
36
|
experiments.each(&:run)
|
data/bin/fast-mcp
ADDED
data/fast.gemspec
CHANGED
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.license = 'MIT'
|
|
18
18
|
|
|
19
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
20
|
-
f.match(%r{^(test|spec|features|docs
|
|
20
|
+
f.match(%r{^(test|spec|experiments|examples|features|docs|assets|stylesheets|site)/})
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
spec.post_install_message = <<~THANKS
|
|
@@ -42,10 +42,8 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
spec.executables = %w[fast fast-experiment]
|
|
43
43
|
spec.require_paths = %w[lib experiments]
|
|
44
44
|
|
|
45
|
-
spec.add_dependency 'astrolabe'
|
|
46
45
|
spec.add_dependency 'coderay'
|
|
47
46
|
spec.add_dependency 'parallel'
|
|
48
|
-
spec.add_dependency 'parser'
|
|
49
47
|
spec.add_dependency 'pg_query'
|
|
50
48
|
|
|
51
49
|
spec.add_development_dependency 'bundler'
|
data/ideia_blog_post.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Empowering LLM Agents with Natural Language to Fast Patterns
|
|
2
|
+
|
|
3
|
+
Today I'm excited to announce a significant set of improvements to `fast`, specifically designed to bridge the gap between human reasoning (and LLM agents) and the technical precision of Ruby AST searching.
|
|
4
|
+
|
|
5
|
+
## The Problem: AST Patterns are Hard
|
|
6
|
+
|
|
7
|
+
Searching through code using AST patterns is incredibly powerful, but constructing the right S-expression can be daunting. Whether you're a human or an AI agent, getting the syntax just right—especially for complex queries—often involves a lot of trial and error.
|
|
8
|
+
|
|
9
|
+
## The Solution: NL-to-Fast Translation
|
|
10
|
+
|
|
11
|
+
We've introduced three new tools to solve this:
|
|
12
|
+
|
|
13
|
+
1. **The `fast-pattern-expert` Skill**: A specialized Gemini CLI skill that provides deep guidance, syntax references, and few-shot examples for translating structural descriptions of Ruby code into valid `Fast` patterns.
|
|
14
|
+
2. **`validate_fast_pattern` MCP Tool**: A new tool for our Model Context Protocol (MCP) server that allows agents to verify their generated patterns before executing them.
|
|
15
|
+
3. **`--validate-pattern` CLI Flag**: A quick way for humans to check if their pattern syntax is correct, checking for balanced nesting and valid token types.
|
|
16
|
+
|
|
17
|
+
### Example in Action:
|
|
18
|
+
**Query:** "Find all classes that inherit from `BaseService` and have a `call` method."
|
|
19
|
+
**Pattern:** `[(class _ (const nil BaseService)) (def call)]`
|
|
20
|
+
|
|
21
|
+
## Robustness and Precision
|
|
22
|
+
|
|
23
|
+
We've also beefed up the core of `fast`:
|
|
24
|
+
- **Stricter Validation**: The `ExpressionParser` now tracks nesting levels and unconsumed tokens, providing helpful error messages instead of silently failing or returning partial matches.
|
|
25
|
+
- **Improved `...` Matcher**: The `...` literal now acts as a "rest-of-children" matcher when used at the end of a pattern, making it much easier to match methods with any number of arguments.
|
|
26
|
+
- **Graceful Degradation**: Multi-file scans (`.scan`) now degrade gracefully if a single file fails to parse, ensuring your reconnaissance isn't aborted by one unsupported syntax node.
|
|
27
|
+
|
|
28
|
+
## A Safer Release Workflow
|
|
29
|
+
|
|
30
|
+
On the operational side, I'm moving our gem release process to **GitHub Actions**. By releasing from a clean CI sandbox instead of my local machine, we ensure a much higher level of security and reproducibility. This move minimizes the risk of local environment contamination and provides a transparent, auditable path from source to RubyGems.
|
|
31
|
+
|
|
32
|
+
## Release 0.2.4
|
|
33
|
+
|
|
34
|
+
To celebrate these features, we are releasing version `0.2.4` today!
|
|
35
|
+
|
|
36
|
+
Happy searching!
|