ffast 0.2.3 → 0.2.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da30bc7abc983e2ac54c1c4d1f454878d75dce0dd285a0e245d6ed54d73e4040
4
- data.tar.gz: f286ce93d5ce2970d1c3a528d423bf8b8ed9239140f798f895a67e0aa1b8e2d2
3
+ metadata.gz: 95125c92ee4201a508f422f041be2a63aebe3a92a19ca303d85b1be98ab6f1e0
4
+ data.tar.gz: 137a7d81790b9f51aaeea271d4d803b6b33a158f7f4b9f4c8a007a23131e6f05
5
5
  SHA512:
6
- metadata.gz: 83c218849cd4a9184b34f9d760d537cae650fe8a96428aa83fdeff12ed6afc57129fff3ce4a0c6d6924225e178f80c114afb021e19ca41bc2677c85b0668ee99
7
- data.tar.gz: 6d4499e8a11398c3f9bc8a04adcd5efd65ef1b385c9bc37370edc31f9375b8d844e8ed61981e33831b2b83961edd4245059a38167c2f7a5ffbc52dd49acb9fff
6
+ metadata.gz: 9b1cb25bf21ef812530856cd778b8d875df7498bc9aaa6576c215964c75dc189131f8fdb539b18eec54b8c44fc3033099b74d457135f77feeff6d5f1ebb34405
7
+ data.tar.gz: 256b3549dd55c412ad1a5d2d84b7d284f4fb94f10096f56a02391ae4e67f4e40894ad1816ab997f7dd08b99b36aa90b9eb68ce3101fa8bba0fb0ba7185329761
@@ -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/fast.gemspec CHANGED
@@ -17,7 +17,8 @@ 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|experiments|examples|features|docs|assets|stylesheets|site)/})
20
+ f.match(%r{^(test|spec|experiments|examples|features|docs|assets|stylesheets|site)/}) ||
21
+ f.match(%r{^(\.git|\.github|\.travis|\.sourcelevel|\.rubocop|\.projections|\.rspec|Gemfile|Rakefile|Guardfile|mkdocs|requirements-docs|TODO|ideia_blog_post)})
21
22
  end
22
23
 
23
24
  spec.post_install_message = <<~THANKS
@@ -39,8 +40,8 @@ Gem::Specification.new do |spec|
39
40
  THANKS
40
41
 
41
42
  spec.bindir = 'bin'
42
- spec.executables = %w[fast fast-experiment]
43
- spec.require_paths = %w[lib experiments]
43
+ spec.executables = %w[fast fast-experiment fast-mcp]
44
+ spec.require_paths = %w[lib]
44
45
 
45
46
  spec.add_dependency 'coderay'
46
47
  spec.add_dependency 'parallel'
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
- output =
24
- if node.respond_to?(:loc) && !show_sexp
25
- if level
26
- Fast.fold_source(node, level: level)
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)
@@ -97,8 +106,23 @@ module Fast
97
106
  args = args.dup
98
107
  args = replace_args_with_shortcut(args) if shortcut_name_from(args)
99
108
  @colorize = STDOUT.isatty
100
- option_parser.parse! args
109
+ @headless = false
110
+ @bodyless = false
111
+ @captures = false
112
+ @parallel = false
113
+ @debug = false
114
+ @sql = false
115
+ @level = nil
116
+ @show_sexp = false
117
+ @help = false
118
+ @similar = false
119
+ @from_code = false
120
+ @show_link = false
121
+ @show_permalink = false
122
+ @files = []
123
+ option_parser.parse!(args)
101
124
  @pattern, @files = extract_pattern_and_files(args)
125
+ puts "DEBUG: pattern=#{@pattern.inspect} files=#{@files.inspect}" if @debug
102
126
 
103
127
  @sql ||= @files.any? && @files.all? { |file| file.end_with?('.sql') }
104
128
  require 'fast/sql' if @sql
@@ -137,7 +161,7 @@ module Fast
137
161
  @sql = true
138
162
  end
139
163
 
140
- opts.on('--captures', 'Print only captures of the patterns and skip node results') do
164
+ opts.on('-c', '--captures', 'Print only captures of the patterns and skip node results') do
141
165
  @captures = true
142
166
  end
143
167
 
@@ -166,6 +190,16 @@ module Fast
166
190
  @from_code = true
167
191
  end
168
192
 
193
+ opts.on('--validate-pattern PATTERN', 'Validate a node pattern') do |pattern|
194
+ begin
195
+ Fast.expression(pattern)
196
+ puts "Pattern is valid."
197
+ rescue StandardError => e
198
+ puts "Invalid pattern: #{e.message}"
199
+ end
200
+ exit
201
+ end
202
+
169
203
  opts.on_tail('--version', 'Show version') do
170
204
  puts Fast::VERSION
171
205
  exit
@@ -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
@@ -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| build_node(:arg, [child.name], child, source, buffer_name) }) if node.respond_to?(:requireds)
235
- children.concat(node.optionals.map { |child| build_node(:optarg, [child.name, adapt(child.value, source, buffer_name)], child, source, buffer_name) }) if node.respond_to?(:optionals)
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| build_node(:arg, [child.name], child, source, buffer_name) }) if node.respond_to?(:posts)
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.name], node, source, buffer_name)
271
+ build_node(:kwarg, [parameter_name(node)], node, source, buffer_name)
255
272
  when Prism::OptionalKeywordParameterNode
256
- build_node(:kwoptarg, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
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.name], node, source, buffer_name)
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
- entries = flatten_entries(Fast.summary(IO.read(file), file: file, command_name: @command_name).outline)
29
- next if entries.empty?
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
- memo[classify(file, entries)] << [file, entries]
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
- Fast.parse_ruby(code_or_ast, buffer_name: file || '(string)')
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fast
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.6'
5
5
  end
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
- if parallel
224
- require 'parallel' unless defined?(Parallel)
225
- Parallel.map(files, &group_files)
226
- else
227
- files.map(&group_files)
228
- end.compact.inject(&:merge!)
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).parse
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 '(' then parse_until_peek(')')
455
- when '{' then Any.new(parse_until_peek('}'))
456
- when '[' then All.new(parse_until_peek(']'))
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
@@ -501,10 +548,15 @@ module Fast
501
548
  case expression
502
549
  when Proc then expression.call(node)
503
550
  when Find then expression.match?(node)
504
- when Symbol then compare_symbol_or_head(expression, node)
505
551
  when Enumerable
506
- expression.each_with_index.all? do |exp, i|
507
- match_recursive(exp, i.zero? ? node : node.children[i - 1])
552
+ if expression.last == :'...' || expression.last.is_a?(Find) && expression.last.token == '...'
553
+ expression[0...-1].each_with_index.all? do |exp, i|
554
+ match_recursive(exp, i.zero? ? node : node.children[i - 1])
555
+ end
556
+ else
557
+ expression.each_with_index.all? do |exp, i|
558
+ match_recursive(exp, i.zero? ? node : node.children[i - 1])
559
+ end
508
560
  end
509
561
  else
510
562
  node == expression
@@ -721,7 +773,7 @@ module Fast
721
773
  # Fast.expression("{int float}")
722
774
  class Any < Find
723
775
  def match?(node)
724
- token.any? { |expression| Fast.match?(expression, node) }
776
+ token.any? { |expression| !!Fast.match?(expression, node) }
725
777
  end
726
778
 
727
779
  def to_s
@@ -732,7 +784,7 @@ module Fast
732
784
  # Intersect expressions. Works like a **AND** operator.
733
785
  class All < Find
734
786
  def match?(node)
735
- token.all? { |expression| expression.match?(node) }
787
+ token.all? { |expression| !!expression.match?(node) }
736
788
  end
737
789
 
738
790
  def to_s
@@ -798,10 +850,17 @@ module Fast
798
850
 
799
851
  # @return [true] if all children matches with tail
800
852
  def match_tail?(tail, child)
801
- tail.each_with_index.all? do |token, i|
802
- prepare_token(token)
803
- token.is_a?(Array) ? match?(token, child[i]) : token.match?(child[i])
804
- end && find_captures
853
+ if tail.last.is_a?(Find) && tail.last.token == '...'
854
+ tail[0...-1].each_with_index.all? do |token, i|
855
+ prepare_token(token)
856
+ token.is_a?(Array) ? match?(token, child[i]) : token.match?(child[i])
857
+ end && find_captures
858
+ else
859
+ tail.each_with_index.all? do |token, i|
860
+ prepare_token(token)
861
+ token.is_a?(Array) ? match?(token, child[i]) : token.match?(child[i])
862
+ end && find_captures
863
+ end
805
864
  end
806
865
 
807
866
  # 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.3
4
+ version: 0.2.6
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-30 00:00:00.000000000 Z
11
+ date: 2026-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: coderay
@@ -240,25 +240,15 @@ email:
240
240
  executables:
241
241
  - fast
242
242
  - fast-experiment
243
+ - fast-mcp
243
244
  extensions: []
244
245
  extra_rdoc_files: []
245
246
  files:
246
- - ".github/workflows/release.yml"
247
- - ".github/workflows/ruby.yml"
248
- - ".gitignore"
249
- - ".projections.json"
250
- - ".rspec"
251
- - ".rubocop.yml"
252
- - ".sourcelevel.yml"
253
- - ".travis.yml"
247
+ - ".agents/fast-pattern-expert/SKILL.md"
254
248
  - CODE_OF_CONDUCT.md
255
249
  - Fastfile
256
- - Gemfile
257
- - Guardfile
258
250
  - LICENSE.txt
259
251
  - README.md
260
- - Rakefile
261
- - TODO.md
262
252
  - bin/console
263
253
  - bin/fast
264
254
  - bin/fast-experiment
@@ -281,8 +271,6 @@ files:
281
271
  - lib/fast/sql/rewriter.rb
282
272
  - lib/fast/summary.rb
283
273
  - lib/fast/version.rb
284
- - mkdocs.yml
285
- - requirements-docs.txt
286
274
  homepage: https://jonatas.github.io/fast/
287
275
  licenses:
288
276
  - MIT
@@ -306,7 +294,6 @@ post_install_message: |2+
306
294
  rdoc_options: []
307
295
  require_paths:
308
296
  - lib
309
- - experiments
310
297
  required_ruby_version: !ruby/object:Gem::Requirement
311
298
  requirements:
312
299
  - - ">="
@@ -1,27 +0,0 @@
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
@@ -1,34 +0,0 @@
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 DELETED
@@ -1,14 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /site/
9
- /spec/reports/
10
- /tmp/
11
- .venv/
12
-
13
- # rspec failure tracking
14
- .rspec_status
data/.projections.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "README.md": { "type": "readme" },
3
- "lib/*.rb": { "type": "lib", "alternate": "spec/{}_spec.rb" }
4
- }
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --format documentation
2
- --color
data/.rubocop.yml DELETED
@@ -1,160 +0,0 @@
1
- # This is the configuration used to check the rubocop source code.
2
-
3
- require:
4
- - rubocop-rspec
5
- - rubocop-performance
6
-
7
- AllCops:
8
- Exclude:
9
- - 'tmp/**/*'
10
- - 'examples/*'
11
- TargetRubyVersion: 2.6
12
-
13
- Layout/LineLength:
14
- Enabled: false
15
-
16
- Layout/EmptyLinesAroundAttributeAccessor:
17
- Enabled: false
18
-
19
- Layout/SpaceAroundMethodCallOperator:
20
- Enabled: false
21
-
22
- Lint/BinaryOperatorWithIdenticalOperands:
23
- Enabled: true
24
-
25
- Lint/DuplicateElsifCondition:
26
- Enabled: false
27
-
28
- Lint/DuplicateRescueException:
29
- Enabled: false
30
-
31
- Lint/EmptyConditionalBody:
32
- Enabled: false
33
-
34
- Lint/FloatComparison:
35
- Enabled: false
36
-
37
- Lint/MissingSuper:
38
- Enabled: false
39
-
40
- Lint/OutOfRangeRegexpRef:
41
- Enabled: false
42
-
43
- Lint/SelfAssignment:
44
- Enabled: false
45
-
46
- Lint/TopLevelReturnWithArgument:
47
- Enabled: false
48
-
49
- Lint/UnreachableLoop:
50
- Enabled: false
51
-
52
-
53
- Lint/DeprecatedOpenSSLConstant:
54
- Enabled: false
55
-
56
- Lint/MixedRegexpCaptureTypes:
57
- Enabled: false
58
-
59
- Lint/RaiseException:
60
- Enabled: true
61
-
62
- Lint/StructNewOverride:
63
- Enabled: true
64
-
65
- Style/AccessorGrouping:
66
- Enabled: false
67
-
68
- Style/ArrayCoercion:
69
- Enabled: false
70
-
71
- Style/BisectedAttrAccessor:
72
- Enabled: true
73
-
74
- Style/CaseLikeIf:
75
- Enabled: true
76
-
77
- Style/ExplicitBlockArgument:
78
- Enabled: false
79
-
80
- Style/ExponentialNotation:
81
- Enabled: true
82
-
83
- Style/GlobalStdStream:
84
- Enabled: false
85
-
86
- Style/HashAsLastArrayItem:
87
- Enabled: false
88
-
89
- Style/HashLikeCase:
90
- Enabled: true
91
-
92
- Style/OptionalBooleanParameter:
93
- Enabled: true
94
-
95
- Style/RedundantAssignment:
96
- Enabled: true
97
-
98
- Style/RedundantFetchBlock:
99
- Enabled: true
100
-
101
- Style/RedundantFileExtensionInRequire:
102
- Enabled: true
103
-
104
- Style/SingleArgumentDig:
105
- Enabled: true
106
-
107
- Style/StringConcatenation:
108
- Enabled: true
109
-
110
- Style/RedundantRegexpCharacterClass:
111
- Enabled: false
112
-
113
- Style/RedundantRegexpEscape:
114
- Enabled: false
115
-
116
- Style/SlicingWithRange:
117
- Enabled: true
118
-
119
- Metrics/BlockLength:
120
- Exclude:
121
- - 'spec/**/*'
122
- - 'fast.gemspec'
123
-
124
- Lint/InterpolationCheck:
125
- Exclude:
126
- - 'spec/**/*'
127
-
128
- Metrics/MethodLength:
129
- CountComments: false # count full line comments?
130
- Max: 12
131
-
132
- Metrics/ModuleLength:
133
- Enabled: false
134
-
135
- Layout/MultilineMethodCallIndentation:
136
- EnforcedStyle: 'indented'
137
-
138
- RSpec/NestedGroups:
139
- Max: 4
140
-
141
- RSpec/ExampleLength:
142
- Max: 20
143
-
144
- RSpec/MultipleExpectations:
145
- Enabled: false
146
-
147
- RSpec/DescribedClass:
148
- Enabled: false
149
-
150
- RSpec/ImplicitSubject:
151
- Enabled: false
152
-
153
- Style/HashEachMethods:
154
- Enabled: true
155
-
156
- Style/HashTransformKeys:
157
- Enabled: true
158
-
159
- Style/HashTransformValues:
160
- Enabled: true
data/.sourcelevel.yml DELETED
@@ -1,2 +0,0 @@
1
- pull_requests:
2
- comments: false
data/.travis.yml DELETED
@@ -1,18 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- env:
4
- global:
5
- - CC_TEST_REPORTER_ID=cf3977cb8c335147723d765c91877e0506ba43e56a22a0dc5b83d7fb969cf5e4
6
- rvm:
7
- - 2.6.3
8
- before_install:
9
- gem install bundler -v 2.1.4
10
- before_script:
11
- - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
12
- - chmod +x ./cc-test-reporter
13
- - ./cc-test-reporter before-build
14
- after_script:
15
- - ./cc-test-reporter after-build -t simplecov --exit-code $TRAVIS_TEST_RESULT
16
- script:
17
- - bundle exec rubocop --fail-level warning --display-only-fail-level-offenses
18
- - bundle exec rspec
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify your gem's dependencies in fast.gemspec
6
- gemspec
data/Guardfile DELETED
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A sample Guardfile
4
- # More info at https://github.com/guard/guard#readme
5
-
6
- ## Uncomment and set this to only include directories you want to watch
7
- # directories %w(app lib config test spec features) \
8
- # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
9
-
10
- ## Note: if you are using the `directories` clause above and you are not
11
- ## watching the project directory ('.'), then you will want to move
12
- ## the Guardfile to a watched dir and symlink it back, e.g.
13
- #
14
- # $ mkdir config
15
- # $ mv Guardfile config/
16
- # $ ln -s config/Guardfile .
17
- #
18
- # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19
-
20
- guard 'livereload' do
21
- watch(%r{lib/.+\.rb$})
22
- end
23
-
24
- guard :rspec, cmd: 'bundle exec rspec' do
25
- require 'guard/rspec/dsl'
26
- dsl = Guard::RSpec::Dsl.new(self)
27
-
28
- # Feel free to open issues for suggestions and improvements
29
-
30
- # RSpec files
31
- rspec = dsl.rspec
32
- watch(rspec.spec_helper) { rspec.spec_dir }
33
- watch(rspec.spec_support) { rspec.spec_dir }
34
- watch(rspec.spec_files)
35
-
36
- # Ruby files
37
- ruby = dsl.ruby
38
- dsl.watch_spec_files_for(ruby.lib_files)
39
- end
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
data/TODO.md DELETED
@@ -1,3 +0,0 @@
1
- - [ ] Add matcher diagnostics. Allow check details of each matcher in the three
2
- - [ ] Split stuff into files and add tests for each class
3
- - [ ] Validate expressions and raise errors for invalid expressions
data/mkdocs.yml DELETED
@@ -1,51 +0,0 @@
1
- site_name: Fast
2
- repo_url: https://github.com/jonatas/fast
3
- edit_uri: edit/master/docs/
4
-
5
- extra:
6
- analytics:
7
- provider: google
8
- property: G-YKZDZDNRG2
9
-
10
- theme:
11
- name: material
12
- palette:
13
- primary: indigo
14
- accent: pink
15
- logo: assets/logo.png
16
- favicon: assets/favicon.png
17
- extra_css:
18
- - stylesheets/custom.css
19
-
20
- plugins:
21
- - search
22
-
23
- markdown_extensions:
24
- - admonition
25
- - pymdownx.details
26
- - pymdownx.superfences
27
- - pymdownx.tabbed:
28
- alternate_style: true
29
- - toc:
30
- permalink: true
31
- nav:
32
- - Introduction: index.md
33
- - Walkthrough: walkthrough.md
34
- - Syntax: syntax.md
35
- - Command Line: command_line.md
36
- - Experiments: experiments.md
37
- - Shortcuts: shortcuts.md
38
- - Git Integration: git.md
39
- - Fast for LLMs and Agents: agents.md
40
- - MCP Server Tutorial: mcp_tutorial.md
41
- - Code Similarity: similarity_tutorial.md
42
- - LLM/Agent Feature TODOs: llm_features.md
43
- - Pry Integration: pry-integration.md
44
- - Editors' Integration: editors-integration.md
45
- - Research: research.md
46
- - Ideas: ideas.md
47
- - Videos: videos.md
48
- - SQL:
49
- - Intro: sql/index.md
50
- - Shortcuts: sql/shortcuts.md
51
- - About: sql-support.md
@@ -1,3 +0,0 @@
1
- mkdocs>=1.6,<2.0
2
- mkdocs-material>=9.5,<10.0
3
- pymdown-extensions>=10.0,<11.0