ffast 0.2.3 → 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/Fastfile +4 -4
- data/README.md +14 -14
- data/ideia_blog_post.md +36 -0
- data/lib/fast/cli.rb +31 -12
- data/lib/fast/mcp_server.rb +24 -0
- data/lib/fast/prism_adapter.rb +23 -6
- data/lib/fast/scan.rb +7 -3
- data/lib/fast/summary.rb +6 -1
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +78 -18
- metadata +4 -2
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.
|
data/Fastfile
CHANGED
|
@@ -15,7 +15,7 @@ version_file = Dir['lib/*/version.rb'].first
|
|
|
15
15
|
Fast.shortcut(:version, '(casgn nil VERSION (str _))', version_file)
|
|
16
16
|
|
|
17
17
|
# Show all classes that inherits Fast::Find
|
|
18
|
-
Fast.shortcut(:finders, '(class ... (const nil Find)', 'lib')
|
|
18
|
+
Fast.shortcut(:finders, '(class ... (const nil Find))', 'lib')
|
|
19
19
|
|
|
20
20
|
# You can run shortcuts appending a dot to the shortcut.
|
|
21
21
|
# $ fast .version
|
|
@@ -23,13 +23,13 @@ Fast.shortcut(:finders, '(class ... (const nil Find)', 'lib')
|
|
|
23
23
|
# VERSION = '0.1.2'
|
|
24
24
|
|
|
25
25
|
# Simple shortcut that I used often to show how the expression parser works
|
|
26
|
-
Fast.shortcut(:parser, '(class (const nil ExpressionParser)', 'lib/fast.rb')
|
|
27
|
-
Fast.shortcut(:sql_parser, '(def parse', 'lib/fast/sql.rb')
|
|
26
|
+
Fast.shortcut(:parser, '(class (const nil ExpressionParser))', 'lib/fast.rb')
|
|
27
|
+
Fast.shortcut(:sql_parser, '(def parse ...)', 'lib/fast/sql.rb')
|
|
28
28
|
|
|
29
29
|
# Use `fast .bump_version` to rewrite the version file
|
|
30
30
|
Fast.shortcut :bump_version do
|
|
31
31
|
new_version = nil
|
|
32
|
-
rewrite_file('(casgn nil VERSION (str _)', version_file) do |node|
|
|
32
|
+
rewrite_file('(casgn nil VERSION (str _))', version_file) do |node|
|
|
33
33
|
target = node.children.last.loc.expression
|
|
34
34
|
pieces = target.source.split('.').map(&:to_i)
|
|
35
35
|
pieces.reverse.each_with_index do |fragment, i|
|
data/README.md
CHANGED
|
@@ -12,20 +12,6 @@ the code was written without an AST.
|
|
|
12
12
|
|
|
13
13
|
Check out the official documentation: https://jonatas.github.io/fast.
|
|
14
14
|
|
|
15
|
-
## Documentation locally
|
|
16
|
-
|
|
17
|
-
The documentation site is built with MkDocs Material and a few Markdown
|
|
18
|
-
extensions. To run it locally:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
python3 -m venv .venv
|
|
22
|
-
source .venv/bin/activate
|
|
23
|
-
python3 -m pip install -r requirements-docs.txt
|
|
24
|
-
mkdocs serve
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Then open `http://127.0.0.1:8000`.
|
|
28
|
-
|
|
29
15
|
## Token Syntax for `find` in AST
|
|
30
16
|
|
|
31
17
|
The current version of Fast covers the following token elements:
|
|
@@ -694,6 +680,20 @@ code("a = 1") # => s(:lvasgn, s(:int, 1))
|
|
|
694
680
|
|
|
695
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).
|
|
696
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
|
+
|
|
697
697
|
## Contributing
|
|
698
698
|
|
|
699
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/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!
|
data/lib/fast/cli.rb
CHANGED
|
@@ -12,6 +12,12 @@ require 'ostruct'
|
|
|
12
12
|
# It defines #report and #highlight functions that can be used to pretty print
|
|
13
13
|
# code and results from the search.
|
|
14
14
|
module Fast
|
|
15
|
+
module SymbolExtension
|
|
16
|
+
def loc
|
|
17
|
+
OpenStruct.new(expression: OpenStruct.new(line: 0))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
module_function
|
|
16
22
|
|
|
17
23
|
# Highligh some source code based on the node.
|
|
@@ -20,19 +26,19 @@ module Fast
|
|
|
20
26
|
# @param colorize [Boolean] skips `CodeRay` processing when false.
|
|
21
27
|
# @param level [Integer] defines the max depth to print the AST.
|
|
22
28
|
def highlight(node, show_sexp: false, colorize: true, sql: false, level: nil)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else
|
|
28
|
-
wrap_source_range(node).source
|
|
29
|
-
end
|
|
30
|
-
elsif show_sexp && level && Fast.ast_node?(node)
|
|
31
|
-
Fast.fold_ast(node, level: level).to_s
|
|
32
|
-
elsif show_sexp
|
|
33
|
-
node.to_s
|
|
29
|
+
output =
|
|
30
|
+
if node.respond_to?(:loc) && !show_sexp
|
|
31
|
+
if level
|
|
32
|
+
Fast.fold_source(node, level: level)
|
|
34
33
|
else
|
|
35
|
-
node
|
|
34
|
+
wrap_source_range(node).source
|
|
35
|
+
end
|
|
36
|
+
elsif show_sexp && level && Fast.ast_node?(node)
|
|
37
|
+
Fast.fold_ast(node, level: level).to_s
|
|
38
|
+
elsif show_sexp
|
|
39
|
+
node.to_s
|
|
40
|
+
else
|
|
41
|
+
node.to_s
|
|
36
42
|
end
|
|
37
43
|
return output unless colorize
|
|
38
44
|
|
|
@@ -78,6 +84,9 @@ module Fast
|
|
|
78
84
|
# Fast.report(result, file: 'file.rb')
|
|
79
85
|
def report(result, show_link: false, show_permalink: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true, level: nil) # rubocop:disable Metrics/ParameterLists
|
|
80
86
|
if file
|
|
87
|
+
if result.is_a?(Symbol) && !result.respond_to?(:loc)
|
|
88
|
+
result.extend(SymbolExtension)
|
|
89
|
+
end
|
|
81
90
|
line = result.loc.expression.line if Fast.ast_node?(result) && result.respond_to?(:loc)
|
|
82
91
|
if show_link
|
|
83
92
|
puts(result.link)
|
|
@@ -166,6 +175,16 @@ module Fast
|
|
|
166
175
|
@from_code = true
|
|
167
176
|
end
|
|
168
177
|
|
|
178
|
+
opts.on('--validate-pattern PATTERN', 'Validate a node pattern') do |pattern|
|
|
179
|
+
begin
|
|
180
|
+
Fast.expression(pattern)
|
|
181
|
+
puts "Pattern is valid."
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
puts "Invalid pattern: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
exit
|
|
186
|
+
end
|
|
187
|
+
|
|
169
188
|
opts.on_tail('--version', 'Show version') do
|
|
170
189
|
puts Fast::VERSION
|
|
171
190
|
exit
|
data/lib/fast/mcp_server.rb
CHANGED
|
@@ -10,6 +10,17 @@ module Fast
|
|
|
10
10
|
# Implements the Model Context Protocol (MCP) server over STDIO.
|
|
11
11
|
class McpServer
|
|
12
12
|
TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'validate_fast_pattern',
|
|
15
|
+
description: 'Validate a Fast AST pattern. Returns true if valid, or a specific syntax error message if invalid.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
pattern: { type: 'string', description: 'Fast AST pattern to validate.' }
|
|
20
|
+
},
|
|
21
|
+
required: ['pattern']
|
|
22
|
+
}
|
|
23
|
+
},
|
|
13
24
|
{
|
|
14
25
|
name: 'search_ruby_ast',
|
|
15
26
|
description: 'Search Ruby files using a Fast AST pattern. Returns file, line range, and source. Use show_ast=true only when you need the s-expression.',
|
|
@@ -146,8 +157,14 @@ module Fast
|
|
|
146
157
|
args = params['arguments'] || {}
|
|
147
158
|
show_ast = args['show_ast'] || false
|
|
148
159
|
|
|
160
|
+
if args['pattern'] && !args['pattern'].start_with?('(', '{', '[') && !args['pattern'].match?(/^[a-z_]+$/)
|
|
161
|
+
raise "Invalid Fast AST pattern: '#{args['pattern']}'. Did you mean to use an s-expression like '(#{args['pattern']})'?"
|
|
162
|
+
end
|
|
163
|
+
|
|
149
164
|
result =
|
|
150
165
|
case tool_name
|
|
166
|
+
when 'validate_fast_pattern'
|
|
167
|
+
execute_validate_pattern(args['pattern'])
|
|
151
168
|
when 'search_ruby_ast'
|
|
152
169
|
execute_search(args['pattern'], args['paths'], show_ast: show_ast)
|
|
153
170
|
when 'ruby_method_source'
|
|
@@ -170,6 +187,13 @@ module Fast
|
|
|
170
187
|
write_error(id, -32603, 'Tool execution failed', e.message)
|
|
171
188
|
end
|
|
172
189
|
|
|
190
|
+
def execute_validate_pattern(pattern)
|
|
191
|
+
Fast.expression(pattern)
|
|
192
|
+
{ valid: true }
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
{ valid: false, error: e.message }
|
|
195
|
+
end
|
|
196
|
+
|
|
173
197
|
def execute_search(pattern, paths, show_ast: false)
|
|
174
198
|
results = []
|
|
175
199
|
on_result = ->(file, matches) do
|
data/lib/fast/prism_adapter.rb
CHANGED
|
@@ -135,8 +135,12 @@ module Fast
|
|
|
135
135
|
build_node(:sym, [node.unescaped], node, source, buffer_name)
|
|
136
136
|
when Prism::StringNode
|
|
137
137
|
build_node(:str, [node.unescaped], node, source, buffer_name)
|
|
138
|
+
when Prism::XStringNode
|
|
139
|
+
build_node(:xstr, [node.unescaped], node, source, buffer_name)
|
|
138
140
|
when Prism::InterpolatedStringNode
|
|
139
141
|
build_node(:dstr, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
|
|
142
|
+
when Prism::InterpolatedXStringNode
|
|
143
|
+
build_node(:dxstr, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
|
|
140
144
|
when Prism::InterpolatedSymbolNode
|
|
141
145
|
build_node(:dsym, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
|
|
142
146
|
when Prism::ArrayNode
|
|
@@ -183,6 +187,8 @@ module Fast
|
|
|
183
187
|
build_node(:if, [adapt(node.predicate, source, buffer_name), adapt(node.statements, source, buffer_name), adapt(node.consequent, source, buffer_name)], node, source, buffer_name)
|
|
184
188
|
when Prism::UnlessNode
|
|
185
189
|
build_node(:if, [adapt(node.predicate, source, buffer_name), adapt(node.consequent, source, buffer_name), adapt(node.statements, source, buffer_name)], node, source, buffer_name)
|
|
190
|
+
when Prism::RescueModifierNode
|
|
191
|
+
build_node(:rescue, [adapt(node.expression, source, buffer_name), build_node(:resbody, [nil, nil, adapt(node.rescue_expression, source, buffer_name)], node, source, buffer_name), nil], node, source, buffer_name)
|
|
186
192
|
when Prism::CaseNode
|
|
187
193
|
children = [adapt(node.predicate, source, buffer_name)]
|
|
188
194
|
children.concat(node.conditions.map { |condition| adapt(condition, source, buffer_name) })
|
|
@@ -231,10 +237,10 @@ module Fast
|
|
|
231
237
|
return build_node(:args, [], nil, source, buffer_name) unless node
|
|
232
238
|
|
|
233
239
|
children = []
|
|
234
|
-
children.concat(node.requireds.map { |child|
|
|
235
|
-
children.concat(node.optionals.map { |child| build_node(:optarg, [child
|
|
240
|
+
children.concat(node.requireds.map { |child| adapt_required_parameter(child, source, buffer_name) }) if node.respond_to?(:requireds)
|
|
241
|
+
children.concat(node.optionals.map { |child| build_node(:optarg, [parameter_name(child), adapt(child.value, source, buffer_name)], child, source, buffer_name) }) if node.respond_to?(:optionals)
|
|
236
242
|
children << build_node(:restarg, [parameter_name(node.rest)], node.rest, source, buffer_name) if node.respond_to?(:rest) && node.rest
|
|
237
|
-
children.concat(node.posts.map { |child|
|
|
243
|
+
children.concat(node.posts.map { |child| adapt_required_parameter(child, source, buffer_name) }) if node.respond_to?(:posts)
|
|
238
244
|
children.concat(node.keywords.map { |child| adapt_keyword_parameter(child, source, buffer_name) }) if node.respond_to?(:keywords)
|
|
239
245
|
children << build_node(:kwrestarg, [parameter_name(node.keyword_rest)], node.keyword_rest, source, buffer_name) if node.respond_to?(:keyword_rest) && node.keyword_rest
|
|
240
246
|
children << build_node(:blockarg, [parameter_name(node.block)], node.block, source, buffer_name) if node.respond_to?(:block) && node.block
|
|
@@ -248,14 +254,25 @@ module Fast
|
|
|
248
254
|
adapt_parameters(params, source, buffer_name)
|
|
249
255
|
end
|
|
250
256
|
|
|
257
|
+
def adapt_required_parameter(child, source, buffer_name)
|
|
258
|
+
if child.is_a?(Prism::MultiTargetNode)
|
|
259
|
+
mlhs_children = child.lefts.map { |c| adapt_required_parameter(c, source, buffer_name) }
|
|
260
|
+
mlhs_children << build_node(:restarg, [parameter_name(child.rest)], child.rest, source, buffer_name) if child.respond_to?(:rest) && child.rest
|
|
261
|
+
mlhs_children.concat(child.rights.map { |c| adapt_required_parameter(c, source, buffer_name) }) if child.respond_to?(:rights)
|
|
262
|
+
build_node(:mlhs, mlhs_children, child, source, buffer_name)
|
|
263
|
+
else
|
|
264
|
+
build_node(:arg, [parameter_name(child)], child, source, buffer_name)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
251
268
|
def adapt_keyword_parameter(node, source, buffer_name)
|
|
252
269
|
case node
|
|
253
270
|
when Prism::RequiredKeywordParameterNode
|
|
254
|
-
build_node(:kwarg, [node
|
|
271
|
+
build_node(:kwarg, [parameter_name(node)], node, source, buffer_name)
|
|
255
272
|
when Prism::OptionalKeywordParameterNode
|
|
256
|
-
build_node(:kwoptarg, [node
|
|
273
|
+
build_node(:kwoptarg, [parameter_name(node), adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
257
274
|
else
|
|
258
|
-
build_node(:arg, [node
|
|
275
|
+
build_node(:arg, [parameter_name(node)], node, source, buffer_name)
|
|
259
276
|
end
|
|
260
277
|
end
|
|
261
278
|
|
data/lib/fast/scan.rb
CHANGED
|
@@ -25,10 +25,14 @@ module Fast
|
|
|
25
25
|
def scan
|
|
26
26
|
files = Fast.ruby_files_from(*@locations)
|
|
27
27
|
grouped = files.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |file, memo|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
begin
|
|
29
|
+
entries = flatten_entries(Fast.summary(IO.read(file), file: file, command_name: @command_name).outline)
|
|
30
|
+
next if entries.empty?
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
memo[classify(file, entries)] << [file, entries]
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
warn "Error scanning #{file}: #{e.message}" if Fast.debugging
|
|
35
|
+
end
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
print_grouped(grouped)
|
data/lib/fast/summary.rb
CHANGED
|
@@ -21,7 +21,12 @@ module Fast
|
|
|
21
21
|
if unsupported_template?
|
|
22
22
|
nil
|
|
23
23
|
elsif code_or_ast.is_a?(String)
|
|
24
|
-
|
|
24
|
+
begin
|
|
25
|
+
Fast.parse_ruby(code_or_ast, buffer_name: file || '(string)')
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
warn "Error parsing #{file || 'source'}: #{e.message}" if Fast.debugging
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
25
30
|
else
|
|
26
31
|
code_or_ast
|
|
27
32
|
end
|
data/lib/fast/version.rb
CHANGED
data/lib/fast.rb
CHANGED
|
@@ -220,12 +220,24 @@ module Fast
|
|
|
220
220
|
# @return [Hash[String, Array]] with files and results
|
|
221
221
|
def group_results(group_files, locations, parallel: true)
|
|
222
222
|
files = ruby_files_from(*locations)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
results =
|
|
224
|
+
if parallel
|
|
225
|
+
require 'parallel' unless defined?(Parallel)
|
|
226
|
+
Parallel.map(files) do |file|
|
|
227
|
+
group_files.call(file)
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
warn "Error processing #{file}: #{e.message}" if Fast.debugging
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
files.map do |file|
|
|
234
|
+
group_files.call(file)
|
|
235
|
+
rescue StandardError => e
|
|
236
|
+
warn "Error processing #{file}: #{e.message}" if Fast.debugging
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
results.compact.inject(&:merge!) || {}
|
|
229
241
|
end
|
|
230
242
|
|
|
231
243
|
# Capture elements from searches in files. Keep in mind you need to use `$`
|
|
@@ -275,7 +287,11 @@ module Fast
|
|
|
275
287
|
end
|
|
276
288
|
|
|
277
289
|
def expression(string)
|
|
278
|
-
ExpressionParser.new(string)
|
|
290
|
+
parser = ExpressionParser.new(string)
|
|
291
|
+
res = parser.parse
|
|
292
|
+
raise SyntaxError, parser.error_message if parser.tokens_left?
|
|
293
|
+
|
|
294
|
+
res
|
|
279
295
|
end
|
|
280
296
|
|
|
281
297
|
attr_accessor :debugging
|
|
@@ -444,6 +460,7 @@ module Fast
|
|
|
444
460
|
# @param expression [String]
|
|
445
461
|
def initialize(expression)
|
|
446
462
|
@tokens = expression.scan TOKENIZER
|
|
463
|
+
@nesting = []
|
|
447
464
|
end
|
|
448
465
|
|
|
449
466
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
@@ -451,22 +468,43 @@ module Fast
|
|
|
451
468
|
# rubocop:disable Metrics/MethodLength
|
|
452
469
|
def parse
|
|
453
470
|
case (token = next_token)
|
|
454
|
-
when '('
|
|
455
|
-
|
|
456
|
-
|
|
471
|
+
when '('
|
|
472
|
+
@nesting << ')'
|
|
473
|
+
parse_until_peek(')')
|
|
474
|
+
when '{'
|
|
475
|
+
@nesting << '}'
|
|
476
|
+
Any.new(parse_until_peek('}'))
|
|
477
|
+
when '['
|
|
478
|
+
@nesting << ']'
|
|
479
|
+
All.new(parse_until_peek(']'))
|
|
480
|
+
when ')', '}', ']'
|
|
481
|
+
raise SyntaxError, "Unexpected token: #{token}"
|
|
457
482
|
when /^"/ then FindString.new(token[1..-2])
|
|
458
483
|
when /^#\w/ then MethodCall.new(token[1..])
|
|
459
484
|
when /^\.\w[\w\d_]+\?/ then InstanceMethodCall.new(token[1..])
|
|
460
485
|
when '$' then Capture.new(parse)
|
|
461
|
-
when '!' then (@tokens.any? ? Not.new(parse) : Find.new(token))
|
|
486
|
+
when '!' then (@tokens.any? && !closing_token?(@tokens.first) ? Not.new(parse) : Find.new(token))
|
|
462
487
|
when '?' then Maybe.new(parse)
|
|
463
488
|
when '^' then Parent.new(parse)
|
|
464
489
|
when '\\' then FindWithCapture.new(parse)
|
|
465
490
|
when /^%\d/ then FindFromArgument.new(token[1..])
|
|
491
|
+
when nil then nil
|
|
466
492
|
else Find.new(token)
|
|
467
493
|
end
|
|
468
494
|
end
|
|
469
495
|
|
|
496
|
+
def tokens_left?
|
|
497
|
+
@tokens.any? || @nesting.any?
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def error_message
|
|
501
|
+
if @tokens.any?
|
|
502
|
+
"Unconsumed tokens after parsing: #{@tokens.join(' ')}"
|
|
503
|
+
elsif @nesting.any?
|
|
504
|
+
"Unclosed nesting: expected #{@nesting.reverse.join(', ')}"
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
470
508
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
471
509
|
# rubocop:enable Metrics/AbcSize
|
|
472
510
|
# rubocop:enable Metrics/MethodLength
|
|
@@ -477,10 +515,19 @@ module Fast
|
|
|
477
515
|
@tokens.shift
|
|
478
516
|
end
|
|
479
517
|
|
|
518
|
+
def closing_token?(token)
|
|
519
|
+
[')', '}', ']'].include?(token)
|
|
520
|
+
end
|
|
521
|
+
|
|
480
522
|
def parse_until_peek(token)
|
|
481
523
|
list = []
|
|
482
524
|
list << parse until @tokens.empty? || @tokens.first == token
|
|
483
|
-
next_token
|
|
525
|
+
last = next_token
|
|
526
|
+
if last == token
|
|
527
|
+
@nesting.pop
|
|
528
|
+
else
|
|
529
|
+
raise SyntaxError, "Expected #{token} but got #{last || 'end of string'}"
|
|
530
|
+
end
|
|
484
531
|
list
|
|
485
532
|
end
|
|
486
533
|
end
|
|
@@ -503,8 +550,14 @@ module Fast
|
|
|
503
550
|
when Find then expression.match?(node)
|
|
504
551
|
when Symbol then compare_symbol_or_head(expression, node)
|
|
505
552
|
when Enumerable
|
|
506
|
-
expression.
|
|
507
|
-
|
|
553
|
+
if expression.last == :'...' || expression.last.is_a?(Find) && expression.last.token == '...'
|
|
554
|
+
expression[0...-1].each_with_index.all? do |exp, i|
|
|
555
|
+
match_recursive(exp, i.zero? ? node : node.children[i - 1])
|
|
556
|
+
end
|
|
557
|
+
else
|
|
558
|
+
expression.each_with_index.all? do |exp, i|
|
|
559
|
+
match_recursive(exp, i.zero? ? node : node.children[i - 1])
|
|
560
|
+
end
|
|
508
561
|
end
|
|
509
562
|
else
|
|
510
563
|
node == expression
|
|
@@ -798,10 +851,17 @@ module Fast
|
|
|
798
851
|
|
|
799
852
|
# @return [true] if all children matches with tail
|
|
800
853
|
def match_tail?(tail, child)
|
|
801
|
-
tail.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
854
|
+
if tail.last.is_a?(Find) && tail.last.token == '...'
|
|
855
|
+
tail[0...-1].each_with_index.all? do |token, i|
|
|
856
|
+
prepare_token(token)
|
|
857
|
+
token.is_a?(Array) ? match?(token, child[i]) : token.match?(child[i])
|
|
858
|
+
end && find_captures
|
|
859
|
+
else
|
|
860
|
+
tail.each_with_index.all? do |token, i|
|
|
861
|
+
prepare_token(token)
|
|
862
|
+
token.is_a?(Array) ? match?(token, child[i]) : token.match?(child[i])
|
|
863
|
+
end && find_captures
|
|
864
|
+
end
|
|
805
865
|
end
|
|
806
866
|
|
|
807
867
|
# Look recursively into @param expression to check if the expression is have
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ffast
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jônatas Davi Paganini
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: coderay
|
|
@@ -243,6 +243,7 @@ executables:
|
|
|
243
243
|
extensions: []
|
|
244
244
|
extra_rdoc_files: []
|
|
245
245
|
files:
|
|
246
|
+
- ".agents/fast-pattern-expert/SKILL.md"
|
|
246
247
|
- ".github/workflows/release.yml"
|
|
247
248
|
- ".github/workflows/ruby.yml"
|
|
248
249
|
- ".gitignore"
|
|
@@ -265,6 +266,7 @@ files:
|
|
|
265
266
|
- bin/fast-mcp
|
|
266
267
|
- bin/setup
|
|
267
268
|
- fast.gemspec
|
|
269
|
+
- ideia_blog_post.md
|
|
268
270
|
- lib/fast.rb
|
|
269
271
|
- lib/fast/cli.rb
|
|
270
272
|
- lib/fast/experiment.rb
|