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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ module Source
5
+ class Buffer
6
+ attr_accessor :source
7
+ attr_reader :name
8
+
9
+ def initialize(name, source: nil)
10
+ @name = name
11
+ @source = source
12
+ end
13
+
14
+ def source_range(begin_pos = 0, end_pos = source.to_s.length)
15
+ Fast::Source.range(self, begin_pos, end_pos)
16
+ end
17
+ end
18
+
19
+ class Range
20
+ attr_reader :begin_pos, :end_pos, :source_buffer
21
+
22
+ def initialize(source_buffer, begin_pos, end_pos)
23
+ @source_buffer = source_buffer
24
+ @begin_pos = begin_pos
25
+ @end_pos = end_pos
26
+ end
27
+
28
+ def begin
29
+ self.class.new(source_buffer, begin_pos, begin_pos)
30
+ end
31
+
32
+ def end
33
+ self.class.new(source_buffer, end_pos, end_pos)
34
+ end
35
+
36
+ def source
37
+ source_buffer.source.to_s[begin_pos...end_pos]
38
+ end
39
+
40
+ def line
41
+ first_line
42
+ end
43
+
44
+ def first_line
45
+ source_buffer.source.to_s[0...begin_pos].count("\n") + 1
46
+ end
47
+
48
+ def last_line
49
+ source_buffer.source.to_s[0...end_pos].count("\n") + 1
50
+ end
51
+
52
+ def column
53
+ last_newline = source_buffer.source.to_s.rindex("\n", begin_pos - 1)
54
+ begin_pos - (last_newline ? last_newline + 1 : 0)
55
+ end
56
+
57
+ def to_range
58
+ begin_pos...end_pos
59
+ end
60
+
61
+ def join(other)
62
+ self.class.new(source_buffer, [begin_pos, other.begin_pos].min, [end_pos, other.end_pos].max)
63
+ end
64
+
65
+ def adjust(begin_pos: 0, end_pos: 0)
66
+ self.class.new(source_buffer, self.begin_pos + begin_pos, self.end_pos + end_pos)
67
+ end
68
+ end
69
+
70
+ class Map
71
+ attr_accessor :expression, :node
72
+
73
+ def initialize(expression)
74
+ @expression = expression
75
+ end
76
+
77
+ def begin
78
+ expression.begin_pos
79
+ end
80
+
81
+ def end
82
+ expression.end_pos
83
+ end
84
+
85
+ def with_expression(new_expression)
86
+ duplicate_with(expression: new_expression)
87
+ end
88
+
89
+ def with_operator(operator)
90
+ duplicate_with(operator: operator)
91
+ end
92
+
93
+ private
94
+
95
+ def duplicate_with(overrides = {})
96
+ copy = dup
97
+ overrides.each { |name, value| copy.instance_variable_set(:"@#{name}", value) }
98
+ copy
99
+ end
100
+ end
101
+
102
+ module_function
103
+
104
+ def buffer(name, source: nil, buffer_class: Fast::Source::Buffer)
105
+ buffer_class.new(name, source: source)
106
+ end
107
+
108
+ def range(buffer, start_pos, end_pos)
109
+ Fast::Source::Range.new(buffer, start_pos, end_pos)
110
+ end
111
+
112
+ def map(expression)
113
+ Fast::Source::Map.new(expression)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'source'
4
+
5
+ module Fast
6
+ class SourceRewriter
7
+ Edit = Struct.new(:kind, :begin_pos, :end_pos, :content, :order, keyword_init: true)
8
+ ClobberingError = Class.new(StandardError)
9
+
10
+ attr_reader :source_buffer
11
+
12
+ def initialize(source_buffer)
13
+ @source_buffer = source_buffer
14
+ @edits = []
15
+ @order = 0
16
+ end
17
+
18
+ def replace(range, content)
19
+ add_edit(:replace, range, content.to_s)
20
+ self
21
+ end
22
+
23
+ def remove(range)
24
+ replace(range, '')
25
+ end
26
+
27
+ def wrap(range, before, after)
28
+ insert_before(range, before) unless before.nil?
29
+ insert_after(range, after) unless after.nil?
30
+ self
31
+ end
32
+
33
+ def insert_before(range, content)
34
+ add_edit(:insert_before, range.begin, content.to_s)
35
+ self
36
+ end
37
+
38
+ def insert_after(range, content)
39
+ add_edit(:insert_after, range.end, content.to_s)
40
+ self
41
+ end
42
+
43
+ def process
44
+ source = source_buffer.source.to_s
45
+ normalized_replacements = normalize_replacements
46
+ return source if normalized_replacements.empty? && insertions.empty?
47
+
48
+ before_insertions = build_insertions(:insert_before)
49
+ after_insertions = build_insertions(:insert_after)
50
+
51
+ result = +''
52
+ cursor = 0
53
+
54
+ normalized_replacements.each do |replacement|
55
+ result << emit_unreplaced_segment(source, cursor, replacement.begin_pos, before_insertions, after_insertions)
56
+ result << before_insertions.fetch(replacement.begin_pos, '')
57
+ result << replacement.content
58
+ result << after_insertions.fetch(replacement.end_pos, '')
59
+ cursor = replacement.end_pos
60
+ end
61
+
62
+ result << emit_unreplaced_segment(source, cursor, source.length, before_insertions, after_insertions)
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :edits
69
+
70
+ def add_edit(kind, range, content)
71
+ edits << Edit.new(
72
+ kind: kind,
73
+ begin_pos: range.begin_pos,
74
+ end_pos: range.end_pos,
75
+ content: content,
76
+ order: next_order
77
+ )
78
+ end
79
+
80
+ def next_order
81
+ @order += 1
82
+ end
83
+
84
+ def insertions
85
+ edits.select { |edit| insertion?(edit) }
86
+ end
87
+
88
+ def replacements
89
+ edits.reject { |edit| insertion?(edit) }
90
+ end
91
+
92
+ def insertion?(edit)
93
+ edit.kind == :insert_before || edit.kind == :insert_after
94
+ end
95
+
96
+ def normalize_replacements
97
+ replacements
98
+ .sort_by { |edit| [edit.begin_pos, edit.end_pos, edit.order] }
99
+ .each_with_object([]) do |edit, normalized|
100
+ previous = normalized.last
101
+ if previous && overlaps?(previous, edit)
102
+ if deletion?(previous) && deletion?(edit)
103
+ previous.end_pos = [previous.end_pos, edit.end_pos].max
104
+ elsif same_range?(previous, edit)
105
+ previous.content = edit.content
106
+ previous.order = edit.order
107
+ else
108
+ raise ClobberingError, "Overlapping rewrite on #{edit.begin_pos}...#{edit.end_pos}"
109
+ end
110
+ else
111
+ normalized << edit.dup
112
+ end
113
+ end
114
+ end
115
+
116
+ def overlaps?(left, right)
117
+ left.begin_pos < right.end_pos && right.begin_pos < left.end_pos
118
+ end
119
+
120
+ def same_range?(left, right)
121
+ left.begin_pos == right.begin_pos && left.end_pos == right.end_pos
122
+ end
123
+
124
+ def deletion?(edit)
125
+ edit.content.empty?
126
+ end
127
+
128
+ def build_insertions(kind)
129
+ edits
130
+ .select { |edit| edit.kind == kind }
131
+ .group_by(&:begin_pos)
132
+ .transform_values do |position_edits|
133
+ position_edits
134
+ .sort_by(&:order)
135
+ .map(&:content)
136
+ .join
137
+ end
138
+ end
139
+
140
+ def emit_unreplaced_segment(source, from, to, before_insertions, after_insertions)
141
+ segment = +''
142
+ cursor = from
143
+
144
+ while cursor < to
145
+ segment << before_insertions.fetch(cursor, '')
146
+ segment << source[cursor]
147
+ cursor += 1
148
+ segment << after_insertions.fetch(cursor, '')
149
+ end
150
+ segment
151
+ end
152
+ end
153
+ end
@@ -4,12 +4,12 @@ module Fast
4
4
  # @see Fast::SQLRewriter
5
5
  # @return string with the content updated in case the pattern matches.
6
6
  def replace(pattern, ast, &replacement)
7
- sql_rewriter_for(pattern, ast, &replacement).rewrite!
7
+ rewriter_for(pattern, ast, &replacement).rewrite!
8
8
  end
9
9
 
10
10
  # @return [Fast::SQL::Rewriter]
11
11
  # @see Fast::Rewriter
12
- def sql_rewriter_for(pattern, ast, &replacement)
12
+ def rewriter_for(pattern, ast, &replacement)
13
13
  rewriter = Rewriter.new
14
14
  rewriter.ast = ast
15
15
  rewriter.search = pattern
@@ -41,24 +41,53 @@ module Fast
41
41
  # @see Fast::Rewriter
42
42
  class Rewriter < Fast::Rewriter
43
43
 
44
+ def rewrite!
45
+ replace_on(*types)
46
+ case ast
47
+ when Array
48
+ rewrite(buffer, ast.first)
49
+ else
50
+ rewrite(buffer, ast)
51
+ end
52
+ end
53
+
54
+ def source
55
+ super ||
56
+ begin
57
+ case ast
58
+ when Array
59
+ ast.first
60
+ else
61
+ ast
62
+ end.location.expression.source_buffer.source
63
+ end
64
+ end
44
65
  # @return [Array<Symbol>] with all types that matches
45
66
  def types
46
- ast.type
67
+ case ast
68
+ when Array
69
+ ast.map(&:type)
70
+ when NilClass
71
+ []
72
+ else
73
+ ast.type
74
+ end
47
75
  end
48
76
 
49
77
  # Generate methods for all affected types.
50
- # Note the strategy is different from parent class, it if matches the root node, it executes otherwise it search pattern on
78
+ # Note the strategy is different from parent class,
79
+ # it will not stop on first match, but will execute the replacement on
51
80
  # all matching elements.
52
81
  # @see Fast.replace
53
82
  def replace_on(*types)
54
83
  types.map do |type|
55
84
  self.instance_exec do
56
- self.class.define_method :"on_#{ast.type}" do |node|
85
+ self.class.define_method :"on_#{type}" do |node|
57
86
  # SQL nodes are not being automatically invoked by the rewriter,
58
87
  # so we need to match the root node and invoke on matching inner elements.
59
- node.search(search).each_with_index do |node, i|
88
+ Fast.search(search, ast).each_with_index do |node, i|
60
89
  @match_index += 1
61
- execute_replacement(node, nil)
90
+ execute_replacement(node, i)
62
91
  end
63
92
  end
64
93
  end
data/lib/fast/sql.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'pg_query'
2
+ require_relative '../fast/source'
2
3
  require_relative 'sql/rewriter'
3
4
 
4
5
  module Fast
@@ -42,10 +43,10 @@ module Fast
42
43
  # s(:val,
43
44
  # s(:string,
44
45
  # s(:str, "hello AST"))))))))
45
- # `s` represents a Fast::Node which is a subclass of Parser::AST::Node and
46
- # has additional methods to access the tokens and location of the node.
46
+ # `s` represents a Fast::Node with additional methods to access the tokens
47
+ # and location of the node.
47
48
  # ast.search(:string).first.location.expression
48
- # => #<Parser::Source::Range (sql) 7...18>
49
+ # => #<Fast::Source::Range (sql) 7...18>
49
50
  def parse_sql(statement, buffer_name: "(sql)")
50
51
  SQL.parse(statement, buffer_name: buffer_name)
51
52
  end
@@ -53,26 +54,26 @@ module Fast
53
54
  # This module contains methods to parse SQL statements and rewrite them.
54
55
  # It uses PGQuery to parse the SQL statements.
55
56
  # It uses Parser to rewrite the SQL statements.
56
- # It uses Parser::Source::Map to map the AST nodes to the SQL tokens.
57
+ # It uses Fast::Source::Map to map the AST nodes to the SQL tokens.
57
58
  #
58
59
  # @example
59
60
  # Fast::SQL.parse("select 1")
60
61
  # => s(:select_stmt, s(:target_list, ...
61
62
  # @see Fast::SQL::Node
62
63
  module SQL
63
- # The SQL source buffer is a subclass of Parser::Source::Buffer
64
+ # The SQL source buffer is a subclass of Fast::Source::Buffer
64
65
  # which contains the tokens of the SQL statement.
65
66
  # When you call `ast.location.expression` it will return a range
66
67
  # which is mapped to the tokens.
67
68
  # @example
68
69
  # ast = Fast::SQL.parse("select 1")
69
- # ast.location.expression # => #<Parser::Source::Range (sql) 0...9>
70
+ # ast.location.expression # => #<Fast::Source::Range (sql) 0...9>
70
71
  # ast.location.expression.source_buffer.tokens
71
72
  # => [
72
73
  # <PgQuery::ScanToken: start: 0, end: 6, token: :SELECT, keyword_kind: :RESERVED_KEYWORD>,
73
74
  # <PgQuery::ScanToken: start: 7, end: 8, token: :ICONST, keyword_kind: :NO_KEYWORD>]
74
75
  # @see Fast::SQL::Node
75
- class SourceBuffer < Parser::Source::Buffer
76
+ class SourceBuffer < Fast::Source::Buffer
76
77
  def tokens
77
78
  @tokens ||= PgQuery.scan(source).first.tokens
78
79
  end
@@ -111,15 +112,13 @@ module Fast
111
112
  return [] if statement.nil?
112
113
  source_buffer = SQL::SourceBuffer.new(buffer_name, source: statement)
113
114
  tree = PgQuery.parse(statement).tree
115
+ first, *, last = source_buffer.tokens
114
116
  stmts = tree.stmts.map do |stmt|
115
- v = clean_structure(stmt.stmt.to_h)
116
- inner_stmt = statement[stmt.stmt_location, stmt.stmt_len]
117
- first, *, last = source_buffer.tokens
118
117
  from = stmt.stmt_location
119
- to = from.zero? ? last.end : from + stmt.stmt_len
120
- expression = Parser::Source::Range.new(source_buffer, from, to)
121
- source_map = Parser::Source::Map.new(expression)
122
- sql_tree_to_ast(v, source_buffer: source_buffer, source_map: source_map)
118
+ to = stmt.stmt_len.zero? ? last.end : from + stmt.stmt_len
119
+ expression = Fast::Source.range(source_buffer, from, to)
120
+ source_map = Fast::Source.map(expression)
121
+ sql_tree_to_ast(clean_structure(stmt.stmt.to_h), source_buffer: source_buffer, source_map: source_map)
123
122
  end.flatten
124
123
  stmts.one? ? stmts.first : stmts
125
124
  end
@@ -150,8 +149,8 @@ module Fast
150
149
  when Hash
151
150
  if (start = obj.delete(:location))
152
151
  if (token = source_buffer.tokens.find{|e|e.start == start})
153
- expression = Parser::Source::Range.new(source_buffer, token.start, token.end)
154
- source_map = Parser::Source::Map.new(expression)
152
+ expression = Fast::Source.range(source_buffer, token.start, token.end)
153
+ source_map = Fast::Source.map(expression)
155
154
  end
156
155
  end
157
156
  obj.map do |key, value|
@@ -164,4 +163,3 @@ module Fast
164
163
  end
165
164
  end
166
165
  end
167
-