ffast 0.2.2 → 0.2.3

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +27 -0
  3. data/.github/workflows/ruby.yml +34 -0
  4. data/.gitignore +2 -0
  5. data/Fastfile +102 -15
  6. data/README.md +21 -7
  7. data/bin/console +1 -1
  8. data/bin/fast-experiment +3 -0
  9. data/bin/fast-mcp +7 -0
  10. data/fast.gemspec +1 -3
  11. data/lib/fast/cli.rb +58 -26
  12. data/lib/fast/experiment.rb +19 -2
  13. data/lib/fast/git.rb +1 -1
  14. data/lib/fast/mcp_server.rb +317 -0
  15. data/lib/fast/node.rb +258 -0
  16. data/lib/fast/prism_adapter.rb +310 -0
  17. data/lib/fast/rewriter.rb +64 -10
  18. data/lib/fast/scan.rb +203 -0
  19. data/lib/fast/shortcut.rb +16 -4
  20. data/lib/fast/source.rb +116 -0
  21. data/lib/fast/source_rewriter.rb +153 -0
  22. data/lib/fast/sql/rewriter.rb +36 -7
  23. data/lib/fast/sql.rb +15 -17
  24. data/lib/fast/summary.rb +435 -0
  25. data/lib/fast/version.rb +1 -1
  26. data/lib/fast.rb +140 -83
  27. data/mkdocs.yml +19 -4
  28. data/requirements-docs.txt +3 -0
  29. metadata +16 -59
  30. data/docs/command_line.md +0 -238
  31. data/docs/editors-integration.md +0 -46
  32. data/docs/experiments.md +0 -155
  33. data/docs/git.md +0 -115
  34. data/docs/ideas.md +0 -70
  35. data/docs/index.md +0 -404
  36. data/docs/pry-integration.md +0 -27
  37. data/docs/research.md +0 -93
  38. data/docs/shortcuts.md +0 -323
  39. data/docs/similarity_tutorial.md +0 -176
  40. data/docs/sql-support.md +0 -253
  41. data/docs/syntax.md +0 -395
  42. data/docs/videos.md +0 -16
  43. data/docs/walkthrough.md +0 -135
  44. data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
  45. data/examples/experimental_replacement.rb +0 -46
  46. data/examples/find_usage.rb +0 -26
  47. data/examples/let_it_be_experiment.rb +0 -11
  48. data/examples/method_complexity.rb +0 -37
  49. data/examples/search_duplicated.rb +0 -15
  50. data/examples/similarity_research.rb +0 -58
  51. data/examples/simple_rewriter.rb +0 -6
  52. data/experiments/let_it_be_experiment.rb +0 -9
  53. data/experiments/remove_useless_hook.rb +0 -9
  54. data/experiments/replace_create_with_build_stubbed.rb +0 -10
data/lib/fast.rb CHANGED
@@ -1,28 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
- require 'astrolabe/builder'
5
- require_relative 'fast/rewriter'
6
-
7
- # suppress output to avoid parser gem warnings'
8
- def suppress_output
9
- original_stdout = $stdout.clone
10
- original_stderr = $stderr.clone
11
- $stderr.reopen File.new('/dev/null', 'w')
12
- $stdout.reopen File.new('/dev/null', 'w')
13
- yield
14
- ensure
15
- $stdout.reopen original_stdout
16
- $stderr.reopen original_stderr
17
- end
18
4
 
19
- suppress_output do
20
- require 'parser'
21
- require 'parser/current'
22
- end
5
+ require_relative 'fast/source'
6
+ require_relative 'fast/node'
7
+ require_relative 'fast/rewriter'
23
8
 
24
9
  # Fast is a tool to help you search in the code through the Abstract Syntax Tree
25
10
  module Fast
11
+ NODE_PARENTS = ObjectSpace::WeakMap.new
12
+
13
+ class SyntaxError < StandardError; end
14
+
26
15
  # Literals are shortcuts allowed inside {ExpressionParser}
27
16
  LITERAL = {
28
17
  '...' => ->(node) { node&.children&.any? },
@@ -67,83 +56,95 @@ module Fast
67
56
  %\d # bind extra arguments to the expression
68
57
  /x.freeze
69
58
 
70
- # Set some convention methods from file.
71
- class Node < Astrolabe::Node
72
- # @return [String] with path of the file or simply buffer name.
73
- def buffer_name
74
- expression.source_buffer.name
59
+ class << self
60
+ def ast_node?(node)
61
+ node.respond_to?(:type) && node.respond_to?(:children)
75
62
  end
76
63
 
77
- # @return [Parser::Source::Range] from the expression
78
- def expression
79
- location.expression
64
+ def prism_ast(content, buffer_name: '(string)')
65
+ require_relative 'fast/prism_adapter'
66
+ result = Fast::PrismAdapter.parse(content, buffer_name: buffer_name)
67
+ return result if result
68
+
69
+ prism_errors = Prism.parse(content).errors
70
+ message = prism_errors.map(&:message).uniq.join("\n")
71
+ raise SyntaxError, message
80
72
  end
81
73
 
82
- # @return [String] with the content of the #expression
83
- def source
84
- expression.source
74
+ def parse_ruby(content, buffer_name: '(string)')
75
+ prism_ast(content, buffer_name: buffer_name)
85
76
  end
86
77
 
87
- # @return [Boolean] true if a file exists with the #buffer_name
88
- def from_file?
89
- File.exist?(buffer_name)
78
+ def parser_ast(content, buffer_name: '(string)')
79
+ prism_ast(content, buffer_name: buffer_name)
90
80
  end
91
81
 
92
- # @return [Array<String>] with authors from the current expression range
93
- def blame_authors
94
- `git blame -L #{expression.first_line},#{expression.last_line} #{buffer_name}`.lines.map do |line|
95
- line.split('(')[1].split(/\d+/).first.strip
96
- end
82
+ def parser_ast_from_file(file)
83
+ @parser_cache ||= {}
84
+ @parser_cache[file] ||=
85
+ begin
86
+ method =
87
+ if file.end_with?('.sql')
88
+ require_relative 'fast/sql' unless respond_to?(:parse_sql)
89
+ :parse_sql
90
+ else
91
+ :parser_ast
92
+ end
93
+ Fast.public_send(method, IO.read(file), buffer_name: file)
94
+ end
97
95
  end
98
96
 
99
- # @return [String] with the first element from #blame_authors
100
- def author
101
- blame_authors.first
97
+ def parse_file(file)
98
+ return parser_ast_from_file(file) if file.end_with?('.sql')
99
+
100
+ @cache ||= {}
101
+ @cache[file] ||=
102
+ begin
103
+ parse_ruby(IO.read(file), buffer_name: file)
104
+ end
102
105
  end
103
106
 
104
- # Search recursively into a node and its children using a pattern.
105
- # @param [String] pattern
106
- # @param [Array] *args extra arguments to interpolate in the pattern.
107
- # @return [Array<Fast::Node>>] with files and results
108
- def search(pattern, *args)
109
- Fast.search(pattern, self, *args)
107
+ def summary(code_or_ast, file: nil, command_name: '.summary', level: nil)
108
+ require_relative 'fast/summary'
109
+ Summary.new(code_or_ast, file: file, command_name: command_name, level: level)
110
110
  end
111
111
 
112
- # Captures elements from search recursively
113
- # @param [String] pattern
114
- # @param [Array] *args extra arguments to interpolate in the pattern.
115
- # @return [Array<Fast::Node>>] with files and results
116
- def capture(pattern, *args)
117
- Fast.capture(pattern, self, *args)
112
+ def scan(locations, command_name: '.scan', level: nil)
113
+ require_relative 'fast/scan'
114
+ Scan.new(locations, command_name: command_name, level: level)
118
115
  end
119
- end
120
116
 
121
- # Custom builder allow us to set a buffer name for each Node
122
- class Builder < Astrolabe::Builder
123
- attr_writer :buffer_name
124
- # Generates {Node} from the given information.
125
- #
126
- # @return [Node] the generated node
127
- def n(type, children, source_map)
128
- Node.new(type, children, location: source_map, buffer_name: @buffer_name)
117
+ def parser_class
118
+ raise NoMethodError, 'Fast.parser_class was removed; Fast now parses Ruby with Prism'
119
+ end
120
+
121
+ def parser_require_path
122
+ raise NoMethodError, 'Fast.parser_require_path was removed; Fast now parses Ruby with Prism'
123
+ end
124
+
125
+ def parser_const_name
126
+ raise NoMethodError, 'Fast.parser_const_name was removed; Fast now parses Ruby with Prism'
127
+ end
128
+
129
+ def parser_version_supported?(const_name)
130
+ raise NoMethodError, "Fast.parser_version_supported?(#{const_name.inspect}) was removed; Fast now parses Ruby with Prism"
129
131
  end
130
- end
131
132
 
132
- class << self
133
133
  # @return [Fast::Node] from the parsed content
134
134
  # @example
135
135
  # Fast.ast("1") # => s(:int, 1)
136
136
  # Fast.ast("a.b") # => s(:send, s(:send, nil, :a), :b)
137
137
  def ast(content, buffer_name: '(string)')
138
- buffer = Parser::Source::Buffer.new(buffer_name)
139
- buffer.source = content
140
- Parser::CurrentRuby.new(builder_for(buffer_name)).parse(buffer)
138
+ parse_ruby(content, buffer_name: buffer_name)
139
+ end
140
+
141
+ def validate_ruby!(content, buffer_name: '(string)')
142
+ prism_ast(content, buffer_name: buffer_name)
143
+ true
141
144
  end
142
145
 
143
146
  def builder_for(buffer_name)
144
- builder = Builder.new
145
- builder.buffer_name = buffer_name
146
- builder
147
+ raise NoMethodError, "Fast.builder_for(#{buffer_name.inspect}) was removed; Fast now parses Ruby with Prism"
147
148
  end
148
149
 
149
150
  # @return [Fast::Node] parsed from file content
@@ -152,18 +153,7 @@ module Fast
152
153
  # @example
153
154
  # Fast.ast_from_file("example.rb") # => s(...)
154
155
  def ast_from_file(file)
155
- @cache ||= {}
156
- @cache[file] ||=
157
- begin
158
- method =
159
- if file.end_with?('.sql')
160
- require_relative 'fast/sql' unless respond_to?(:parse_sql)
161
- :parse_sql
162
- else
163
- :ast
164
- end
165
- Fast.public_send(method, IO.read(file), buffer_name: file)
166
- end
156
+ parse_file(file)
167
157
  end
168
158
 
169
159
  # Verify if a given AST matches with a specific pattern
@@ -330,6 +320,73 @@ module Fast
330
320
  files.reject(&dir_filter)
331
321
  end
332
322
 
323
+ # Folds the AST to a maximum depth, replacing deeper branches with `...`
324
+ # @param node [Fast::Node]
325
+ # @param level [Integer] maximum depth to explore
326
+ # @param current_level [Integer] internal tracker for depth
327
+ # @return [Fast::Node] the folded AST
328
+ def fold_ast(node, level: nil, current_level: 1)
329
+ return node unless ast_node?(node) && node.respond_to?(:updated)
330
+ return node if level.nil?
331
+
332
+ if current_level >= level
333
+ if node.children.any?
334
+ node.updated(nil, [:'...'])
335
+ else
336
+ node
337
+ end
338
+ else
339
+ new_children = node.children.map { |c| fold_ast(c, level: level, current_level: current_level + 1) }
340
+ node.updated(nil, new_children)
341
+ end
342
+ end
343
+
344
+ # Folds ruby source code to a maximum depth, replacing deep block bodies with `# ...`
345
+ # @param node [Fast::Node]
346
+ # @param level [Integer] maximum depth to explore
347
+ # @return [String] the folded source representation
348
+ def fold_source(node, level: 1)
349
+ return node if node.is_a?(String)
350
+ return node.loc.expression.source rescue node.to_s unless ast_node?(node) && node.respond_to?(:loc)
351
+
352
+ source = node.loc.expression.source.dup
353
+ root_begin = node.loc.expression.begin_pos
354
+
355
+ regions = []
356
+
357
+ walker = ->(n, current_level) do
358
+ return unless ast_node?(n)
359
+
360
+ if current_level >= level
361
+ body_node = nil
362
+ case n.type
363
+ when :class, :module, :def, :defs, :block
364
+ body_node = n.children.last
365
+ when :begin
366
+ body_node = n
367
+ end
368
+
369
+ if body_node && body_node.loc && body_node.loc.respond_to?(:expression) && body_node.loc.expression
370
+ regions << [
371
+ body_node.loc.expression.begin_pos - root_begin,
372
+ body_node.loc.expression.end_pos - root_begin
373
+ ]
374
+ return
375
+ end
376
+ end
377
+
378
+ n.children.each { |c| walker.call(c, current_level + 1) if ast_node?(c) }
379
+ end
380
+
381
+ walker.call(node, 1)
382
+
383
+ regions.sort_by { |r| -r[0] }.each do |start_pos, end_pos|
384
+ source[start_pos...end_pos] = "# ..."
385
+ end
386
+
387
+ source
388
+ end
389
+
333
390
  # Extracts a node pattern expression from a given node supressing identifiers and primitive types.
334
391
  # Useful to index abstract patterns or similar code structure.
335
392
  # @see https://jonatas.github.io/fast/similarity_tutorial/
@@ -341,7 +398,7 @@ module Fast
341
398
  # Fast.expression_from(Fast.ast('def name; person.name end')) # => '(def _ (args) (send (send nil _) _))'
342
399
  def expression_from(node)
343
400
  case node
344
- when Parser::AST::Node
401
+ when ->(candidate) { ast_node?(candidate) }
345
402
  children_expression = node.children.map(&method(:expression_from)).join(' ')
346
403
  "(#{node.type}#{" #{children_expression}" if node.children.any?})"
347
404
  when nil, 'nil'
@@ -456,7 +513,7 @@ module Fast
456
513
 
457
514
  def compare_symbol_or_head(expression, node)
458
515
  case node
459
- when Parser::AST::Node
516
+ when ->(candidate) { Fast.ast_node?(candidate) }
460
517
  node.type == expression.to_sym
461
518
  when String
462
519
  node == expression.to_s
data/mkdocs.yml CHANGED
@@ -1,7 +1,11 @@
1
1
  site_name: Fast
2
2
  repo_url: https://github.com/jonatas/fast
3
3
  edit_uri: edit/master/docs/
4
- google_analytics: ['G-YKZDZDNRG2', 'auto']
4
+
5
+ extra:
6
+ analytics:
7
+ provider: google
8
+ property: G-YKZDZDNRG2
5
9
 
6
10
  theme:
7
11
  name: material
@@ -13,10 +17,15 @@ theme:
13
17
  extra_css:
14
18
  - stylesheets/custom.css
15
19
 
20
+ plugins:
21
+ - search
22
+
16
23
  markdown_extensions:
17
24
  - admonition
18
- - codehilite:
19
- guess_lang: false
25
+ - pymdownx.details
26
+ - pymdownx.superfences
27
+ - pymdownx.tabbed:
28
+ alternate_style: true
20
29
  - toc:
21
30
  permalink: true
22
31
  nav:
@@ -27,10 +36,16 @@ nav:
27
36
  - Experiments: experiments.md
28
37
  - Shortcuts: shortcuts.md
29
38
  - Git Integration: git.md
39
+ - Fast for LLMs and Agents: agents.md
40
+ - MCP Server Tutorial: mcp_tutorial.md
30
41
  - Code Similarity: similarity_tutorial.md
42
+ - LLM/Agent Feature TODOs: llm_features.md
31
43
  - Pry Integration: pry-integration.md
32
44
  - Editors' Integration: editors-integration.md
33
45
  - Research: research.md
34
46
  - Ideas: ideas.md
35
47
  - Videos: videos.md
36
- - SQL Support: sql-support.md
48
+ - SQL:
49
+ - Intro: sql/index.md
50
+ - Shortcuts: sql/shortcuts.md
51
+ - About: sql-support.md
@@ -0,0 +1,3 @@
1
+ mkdocs>=1.6,<2.0
2
+ mkdocs-material>=9.5,<10.0
3
+ pymdown-extensions>=10.0,<11.0
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jônatas Davi Paganini
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-17 00:00:00.000000000 Z
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: astrolabe
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: coderay
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +38,6 @@ dependencies:
52
38
  - - ">="
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: parser
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
41
  - !ruby/object:Gem::Dependency
70
42
  name: pg_query
71
43
  requirement: !ruby/object:Gem::Requirement
@@ -271,6 +243,8 @@ executables:
271
243
  extensions: []
272
244
  extra_rdoc_files: []
273
245
  files:
246
+ - ".github/workflows/release.yml"
247
+ - ".github/workflows/ruby.yml"
274
248
  - ".gitignore"
275
249
  - ".projections.json"
276
250
  - ".rspec"
@@ -288,43 +262,27 @@ files:
288
262
  - bin/console
289
263
  - bin/fast
290
264
  - bin/fast-experiment
265
+ - bin/fast-mcp
291
266
  - bin/setup
292
- - docs/command_line.md
293
- - docs/editors-integration.md
294
- - docs/experiments.md
295
- - docs/git.md
296
- - docs/ideas.md
297
- - docs/index.md
298
- - docs/pry-integration.md
299
- - docs/research.md
300
- - docs/shortcuts.md
301
- - docs/similarity_tutorial.md
302
- - docs/sql-support.md
303
- - docs/syntax.md
304
- - docs/videos.md
305
- - docs/walkthrough.md
306
- - examples/build_stubbed_and_let_it_be_experiment.rb
307
- - examples/experimental_replacement.rb
308
- - examples/find_usage.rb
309
- - examples/let_it_be_experiment.rb
310
- - examples/method_complexity.rb
311
- - examples/search_duplicated.rb
312
- - examples/similarity_research.rb
313
- - examples/simple_rewriter.rb
314
- - experiments/let_it_be_experiment.rb
315
- - experiments/remove_useless_hook.rb
316
- - experiments/replace_create_with_build_stubbed.rb
317
267
  - fast.gemspec
318
268
  - lib/fast.rb
319
269
  - lib/fast/cli.rb
320
270
  - lib/fast/experiment.rb
321
271
  - lib/fast/git.rb
272
+ - lib/fast/mcp_server.rb
273
+ - lib/fast/node.rb
274
+ - lib/fast/prism_adapter.rb
322
275
  - lib/fast/rewriter.rb
276
+ - lib/fast/scan.rb
323
277
  - lib/fast/shortcut.rb
278
+ - lib/fast/source.rb
279
+ - lib/fast/source_rewriter.rb
324
280
  - lib/fast/sql.rb
325
281
  - lib/fast/sql/rewriter.rb
282
+ - lib/fast/summary.rb
326
283
  - lib/fast/version.rb
327
284
  - mkdocs.yml
285
+ - requirements-docs.txt
328
286
  homepage: https://jonatas.github.io/fast/
329
287
  licenses:
330
288
  - MIT
@@ -360,9 +318,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
360
318
  - !ruby/object:Gem::Version
361
319
  version: '0'
362
320
  requirements: []
363
- rubygems_version: 3.4.22
364
- signing_key:
321
+ rubygems_version: 3.5.22
322
+ signing_key:
365
323
  specification_version: 4
366
324
  summary: 'FAST: Find by AST.'
367
325
  test_files: []
368
- ...