ffast 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agents/fast-pattern-expert/SKILL.md +71 -0
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +2 -0
- data/Fastfile +105 -18
- data/README.md +21 -7
- data/bin/console +1 -1
- data/bin/fast-experiment +3 -0
- data/bin/fast-mcp +7 -0
- data/fast.gemspec +1 -3
- data/ideia_blog_post.md +36 -0
- data/lib/fast/cli.rb +74 -23
- data/lib/fast/experiment.rb +19 -2
- data/lib/fast/git.rb +1 -1
- data/lib/fast/mcp_server.rb +341 -0
- data/lib/fast/node.rb +258 -0
- data/lib/fast/prism_adapter.rb +327 -0
- data/lib/fast/rewriter.rb +64 -10
- data/lib/fast/scan.rb +207 -0
- data/lib/fast/shortcut.rb +16 -4
- data/lib/fast/source.rb +116 -0
- data/lib/fast/source_rewriter.rb +153 -0
- data/lib/fast/sql/rewriter.rb +36 -7
- data/lib/fast/sql.rb +15 -17
- data/lib/fast/summary.rb +440 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +218 -101
- data/mkdocs.yml +19 -4
- data/requirements-docs.txt +3 -0
- metadata +18 -59
- data/docs/command_line.md +0 -238
- data/docs/editors-integration.md +0 -46
- data/docs/experiments.md +0 -155
- data/docs/git.md +0 -115
- data/docs/ideas.md +0 -70
- data/docs/index.md +0 -404
- data/docs/pry-integration.md +0 -27
- data/docs/research.md +0 -93
- data/docs/shortcuts.md +0 -323
- data/docs/similarity_tutorial.md +0 -176
- data/docs/sql-support.md +0 -253
- data/docs/syntax.md +0 -395
- data/docs/videos.md +0 -16
- data/docs/walkthrough.md +0 -135
- data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
- data/examples/experimental_replacement.rb +0 -46
- data/examples/find_usage.rb +0 -26
- data/examples/let_it_be_experiment.rb +0 -11
- data/examples/method_complexity.rb +0 -37
- data/examples/search_duplicated.rb +0 -15
- data/examples/similarity_research.rb +0 -58
- data/examples/simple_rewriter.rb +0 -6
- data/experiments/let_it_be_experiment.rb +0 -9
- data/experiments/remove_useless_hook.rb +0 -9
- data/experiments/replace_create_with_build_stubbed.rb +0 -10
data/lib/fast/sql/rewriter.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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_#{
|
|
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
|
-
|
|
88
|
+
Fast.search(search, ast).each_with_index do |node, i|
|
|
60
89
|
@match_index += 1
|
|
61
|
-
execute_replacement(node,
|
|
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
|
|
46
|
-
#
|
|
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
|
-
# => #<
|
|
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
|
|
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
|
|
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 # => #<
|
|
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 <
|
|
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 =
|
|
120
|
-
expression =
|
|
121
|
-
source_map =
|
|
122
|
-
sql_tree_to_ast(
|
|
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 =
|
|
154
|
-
source_map =
|
|
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
|
-
|
data/lib/fast/summary.rb
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fast/prism_adapter'
|
|
4
|
+
|
|
5
|
+
module Fast
|
|
6
|
+
class Summary
|
|
7
|
+
VISIBILITIES = %i[public protected private].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(code_or_ast, file: nil, command_name: '.summary', level: nil)
|
|
10
|
+
@file = file
|
|
11
|
+
@command_name = command_name
|
|
12
|
+
@level = normalize_level(level)
|
|
13
|
+
@source =
|
|
14
|
+
if code_or_ast.is_a?(String)
|
|
15
|
+
code_or_ast
|
|
16
|
+
elsif code_or_ast.respond_to?(:loc) && code_or_ast.loc.respond_to?(:expression)
|
|
17
|
+
code_or_ast.loc.expression.source
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@ast =
|
|
21
|
+
if unsupported_template?
|
|
22
|
+
nil
|
|
23
|
+
elsif code_or_ast.is_a?(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
|
|
30
|
+
else
|
|
31
|
+
code_or_ast
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def summarize
|
|
36
|
+
if @ast
|
|
37
|
+
print_node(@ast)
|
|
38
|
+
elsif unsupported_template?
|
|
39
|
+
puts "Unsupported template format for #{@command_name}: #{File.extname(@file)}"
|
|
40
|
+
else
|
|
41
|
+
puts "Unable to parse #{@file || 'source'} for #{@command_name}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def outline
|
|
46
|
+
return [] unless @ast
|
|
47
|
+
|
|
48
|
+
top_level_nodes(@ast).filter_map { |node| outline_for(node) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def unsupported_template?
|
|
54
|
+
@file && !File.extname(@file).empty? && File.extname(@file) != '.rb'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def print_node(node, indent = '')
|
|
58
|
+
return unless Fast.ast_node?(node)
|
|
59
|
+
|
|
60
|
+
case node.type
|
|
61
|
+
when :module
|
|
62
|
+
puts "#{indent}module #{node_source(node.children[0])}"
|
|
63
|
+
summarize_body(node.children[1], indent + ' ')
|
|
64
|
+
puts "#{indent}end"
|
|
65
|
+
when :class
|
|
66
|
+
name = node_source(node.children[0])
|
|
67
|
+
superclass = node.children[1] ? " < #{node_source(node.children[1])}" : ''
|
|
68
|
+
puts "#{indent}class #{name}#{superclass}"
|
|
69
|
+
summarize_body(node.children[2], indent + ' ')
|
|
70
|
+
puts "#{indent}end"
|
|
71
|
+
when :begin
|
|
72
|
+
summarize_body(node, indent)
|
|
73
|
+
else
|
|
74
|
+
summarize_body(node, indent)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def top_level_nodes(node)
|
|
79
|
+
return [] unless Fast.ast_node?(node)
|
|
80
|
+
|
|
81
|
+
case node.type
|
|
82
|
+
when :begin
|
|
83
|
+
node.children.select { |child| Fast.ast_node?(child) }
|
|
84
|
+
else
|
|
85
|
+
[node]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def outline_for(node)
|
|
90
|
+
return unless Fast.ast_node?(node)
|
|
91
|
+
|
|
92
|
+
case node.type
|
|
93
|
+
when :module
|
|
94
|
+
summary = build_summary(node.children[1])
|
|
95
|
+
build_outline_entry(node, summary, kind: :module, name: node_source(node.children[0]))
|
|
96
|
+
when :class
|
|
97
|
+
summary = build_summary(node.children[2])
|
|
98
|
+
build_outline_entry(node, summary,
|
|
99
|
+
kind: :class,
|
|
100
|
+
name: node_source(node.children[0]),
|
|
101
|
+
superclass: node.children[1] && node_source(node.children[1]))
|
|
102
|
+
else
|
|
103
|
+
summary = build_summary(node)
|
|
104
|
+
build_outline_entry(node, summary, kind: node.type, name: node.type.to_s)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_outline_entry(node, summary, kind:, name:, superclass: nil)
|
|
109
|
+
{
|
|
110
|
+
file: @file,
|
|
111
|
+
kind: kind,
|
|
112
|
+
name: name,
|
|
113
|
+
superclass: superclass,
|
|
114
|
+
headline: outline_headline(kind, name, superclass),
|
|
115
|
+
constants: summary[:constants],
|
|
116
|
+
mixins: summary[:mixins],
|
|
117
|
+
relationships: summary[:relationships],
|
|
118
|
+
attributes: summary[:attributes],
|
|
119
|
+
scopes: summary[:scopes],
|
|
120
|
+
hooks: summary[:hooks],
|
|
121
|
+
validations: summary[:validations],
|
|
122
|
+
macros: summary[:macros],
|
|
123
|
+
requires: summary[:requires],
|
|
124
|
+
methods: summary[:methods],
|
|
125
|
+
nested: summary[:nested].filter_map { |child| outline_for(child) },
|
|
126
|
+
line: node.loc&.expression&.line
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def outline_headline(kind, name, superclass)
|
|
131
|
+
case kind
|
|
132
|
+
when :module
|
|
133
|
+
"module #{name}"
|
|
134
|
+
when :class
|
|
135
|
+
superclass ? "class #{name} < #{superclass}" : "class #{name}"
|
|
136
|
+
else
|
|
137
|
+
name.to_s
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def summarize_body(body, indent)
|
|
142
|
+
return unless Fast.ast_node?(body)
|
|
143
|
+
|
|
144
|
+
summary = build_summary(body)
|
|
145
|
+
|
|
146
|
+
if show_signals?
|
|
147
|
+
print_requires(summary[:requires], indent)
|
|
148
|
+
print_lines(summary[:constants], indent)
|
|
149
|
+
print_lines(summary[:mixins], indent)
|
|
150
|
+
print_lines(summary[:relationships], indent)
|
|
151
|
+
print_lines(summary[:attributes], indent)
|
|
152
|
+
print_section('Scopes', summary[:scopes], indent)
|
|
153
|
+
print_section('Hooks', summary[:hooks], indent)
|
|
154
|
+
print_section('Validations', summary[:validations], indent)
|
|
155
|
+
print_section('Macros', summary[:macros], indent)
|
|
156
|
+
end
|
|
157
|
+
print_methods(summary[:methods], indent) if show_methods?
|
|
158
|
+
summary[:nested].each { |child| print_node(child, indent) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def build_summary(body)
|
|
162
|
+
summary = {
|
|
163
|
+
constants: [],
|
|
164
|
+
mixins: [],
|
|
165
|
+
relationships: [],
|
|
166
|
+
attributes: [],
|
|
167
|
+
scopes: [],
|
|
168
|
+
hooks: [],
|
|
169
|
+
validations: [],
|
|
170
|
+
macros: [],
|
|
171
|
+
requires: [],
|
|
172
|
+
methods: VISIBILITIES.to_h { |visibility| [visibility, []] },
|
|
173
|
+
nested: []
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
visibility = :public
|
|
177
|
+
body_nodes(body).each do |node|
|
|
178
|
+
next unless Fast.ast_node?(node)
|
|
179
|
+
|
|
180
|
+
case node.type
|
|
181
|
+
when :class, :module
|
|
182
|
+
summary[:nested] << node
|
|
183
|
+
when :casgn
|
|
184
|
+
summary[:constants] << constant_line(node)
|
|
185
|
+
when :def
|
|
186
|
+
summary[:methods][visibility] << method_signature(node)
|
|
187
|
+
when :defs
|
|
188
|
+
summary[:methods][visibility] << singleton_method_signature(node)
|
|
189
|
+
when :sclass
|
|
190
|
+
summarize_singleton_class(node, summary, visibility)
|
|
191
|
+
when :send
|
|
192
|
+
visibility = visibility_change(node) || visibility
|
|
193
|
+
categorize_send(node, summary)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
summary
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def summarize_singleton_class(node, summary, default_visibility)
|
|
201
|
+
visibility = default_visibility
|
|
202
|
+
body_nodes(node.children[1]).each do |child|
|
|
203
|
+
next unless Fast.ast_node?(child)
|
|
204
|
+
|
|
205
|
+
case child.type
|
|
206
|
+
when :def
|
|
207
|
+
summary[:methods][visibility] << "def self.#{method_signature(child).delete_prefix('def ')}"
|
|
208
|
+
when :send
|
|
209
|
+
visibility = visibility_change(child) || visibility
|
|
210
|
+
categorize_send(child, summary)
|
|
211
|
+
when :class, :module
|
|
212
|
+
summary[:nested] << child
|
|
213
|
+
when :casgn
|
|
214
|
+
summary[:constants] << constant_line(child)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def categorize_send(node, summary)
|
|
220
|
+
return unless node.type == :send && node.children[0].nil?
|
|
221
|
+
|
|
222
|
+
method_name = node.children[1]
|
|
223
|
+
if Fast.match?('(send nil {has_many belongs_to has_one has_and_belongs_to_many} ...)', node)
|
|
224
|
+
summary[:relationships] << compact_node_source(node)
|
|
225
|
+
elsif Fast.match?('(send nil {attr_accessor attr_reader attr_writer} ...)', node)
|
|
226
|
+
summary[:attributes] << attribute_line(node)
|
|
227
|
+
elsif Fast.match?('(send nil {include extend prepend} ...)', node)
|
|
228
|
+
summary[:mixins] << compact_node_source(node)
|
|
229
|
+
elsif Fast.match?('(send nil scope ...)', node)
|
|
230
|
+
summary[:scopes] << scope_line(node)
|
|
231
|
+
elsif Fast.match?('(send nil validates ...)', node)
|
|
232
|
+
summary[:validations] << node_source(node).delete_prefix('validates ')
|
|
233
|
+
elsif Fast.match?('(send nil validate ...)', node)
|
|
234
|
+
summary[:validations] << node_source(node).delete_prefix('validate ')
|
|
235
|
+
elsif Fast.match?('(send nil {require require_relative} (str _))', node)
|
|
236
|
+
summary[:requires] << required_path(node)
|
|
237
|
+
elsif Fast.match?('(send nil {private protected public})', node)
|
|
238
|
+
nil
|
|
239
|
+
else
|
|
240
|
+
summary[:hooks] << compact_node_source(node) if callback_macro?(method_name)
|
|
241
|
+
summary[:macros] << compact_node_source(node) if macro_candidate?(node, summary)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def macro_candidate?(node, summary)
|
|
246
|
+
return false unless node.type == :send && node.children[0].nil?
|
|
247
|
+
|
|
248
|
+
name = node.children[1]
|
|
249
|
+
return false if callback_macro?(name)
|
|
250
|
+
return false if Fast.match?('(send nil {has_many belongs_to has_one has_and_belongs_to_many} ...)', node)
|
|
251
|
+
return false if Fast.match?('(send nil {attr_accessor attr_reader attr_writer} ...)', node)
|
|
252
|
+
return false if Fast.match?('(send nil {include extend prepend} ...)', node)
|
|
253
|
+
return false if Fast.match?('(send nil {require require_relative} (str _))', node)
|
|
254
|
+
return false if Fast.match?('(send nil {scope validates validate private protected public} ...)', node)
|
|
255
|
+
|
|
256
|
+
!summary[:macros].include?(compact_node_source(node))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def callback_macro?(method_name)
|
|
260
|
+
method_name.to_s.start_with?('before_', 'after_', 'around_')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def visibility_change(node)
|
|
264
|
+
return unless node.type == :send && node.children[0].nil?
|
|
265
|
+
return unless VISIBILITIES.include?(node.children[1])
|
|
266
|
+
return unless node.children.length == 2
|
|
267
|
+
|
|
268
|
+
node.children[1]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def body_nodes(node)
|
|
272
|
+
return [] unless node
|
|
273
|
+
return node.children if node.type == :begin
|
|
274
|
+
|
|
275
|
+
[node]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def normalize_level(level)
|
|
279
|
+
return 3 if level.nil?
|
|
280
|
+
|
|
281
|
+
[[level.to_i, 1].max, 3].min
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def show_signals?
|
|
285
|
+
@level >= 2
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def show_methods?
|
|
289
|
+
@level >= 3
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def constant_line(node)
|
|
293
|
+
lhs = node_source(node.children[0])
|
|
294
|
+
name = node.children[1]
|
|
295
|
+
rhs = node.children[2]
|
|
296
|
+
target = lhs.nil? || lhs.empty? || lhs == 'nil' ? name.to_s : "#{lhs}::#{name}"
|
|
297
|
+
rhs ? "#{target} = #{compact_value(rhs)}" : target
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def attribute_line(node)
|
|
301
|
+
method_name, = captures_for('(send nil $_ ...)', node)
|
|
302
|
+
args = direct_symbol_arguments(node).map { |symbol| ":#{symbol}" }
|
|
303
|
+
"#{method_name} #{args.join(', ')}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def required_path(node)
|
|
307
|
+
path_node = captures_for('(send nil $_ $(str _))', node).last
|
|
308
|
+
path_node.children.first.inspect
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def scope_line(node)
|
|
312
|
+
name = captures_for('(send nil :scope (sym $_) ...)', node).first
|
|
313
|
+
lambda_node = captures_for('(send nil :scope (sym _) $({lambda block} ... ...))', node).first
|
|
314
|
+
args = lambda_args(lambda_node)
|
|
315
|
+
[name, args].join
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def captures_for(pattern, node)
|
|
319
|
+
Fast.match?(pattern, node) || []
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def lambda_args(node)
|
|
323
|
+
return '' unless Fast.ast_node?(node)
|
|
324
|
+
return '' unless node.type == :lambda || node.type == :block
|
|
325
|
+
|
|
326
|
+
args_node =
|
|
327
|
+
if node.type == :block
|
|
328
|
+
node.children.find { |child| Fast.match?('(args ...)', child) }
|
|
329
|
+
else
|
|
330
|
+
node.children[0]
|
|
331
|
+
end
|
|
332
|
+
return '' unless Fast.match?('(args ...)', args_node) || Fast.match?('(args)', args_node)
|
|
333
|
+
return '' if args_node.children.empty?
|
|
334
|
+
|
|
335
|
+
"(#{args_node.children.map { |arg| node_source(arg) }.join(', ')})"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def direct_symbol_arguments(node)
|
|
339
|
+
node.children.drop(2).filter_map do |child|
|
|
340
|
+
captures = captures_for('(sym $_)', child)
|
|
341
|
+
captures.first if captures.any?
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def method_signature(node)
|
|
346
|
+
args = args_signature(node.children[1])
|
|
347
|
+
"def #{node.children[0]}#{args}"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def singleton_method_signature(node)
|
|
351
|
+
receiver = node_source(node.children[0])
|
|
352
|
+
args = args_signature(node.children[2])
|
|
353
|
+
"def #{receiver}.#{node.children[1]}#{args}"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def args_signature(args_node)
|
|
357
|
+
return '' unless Fast.match?('(args ...)', args_node) || Fast.match?('(args)', args_node)
|
|
358
|
+
return '' if args_node.children.empty?
|
|
359
|
+
|
|
360
|
+
"(#{args_node.children.map { |arg| node_source(arg) }.join(', ')})"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def compact_value(node)
|
|
364
|
+
return node_source(node) unless Fast.ast_node?(node)
|
|
365
|
+
|
|
366
|
+
case node.type
|
|
367
|
+
when :array
|
|
368
|
+
'[...]'
|
|
369
|
+
when :hash
|
|
370
|
+
'{...}'
|
|
371
|
+
when :block, :lambda
|
|
372
|
+
'{ ... }'
|
|
373
|
+
when :send
|
|
374
|
+
return compact_value(node.children[0]) if node.children[1] == :freeze && node.children.length == 2
|
|
375
|
+
|
|
376
|
+
node_source(node)
|
|
377
|
+
else
|
|
378
|
+
node_source(node)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def node_source(node)
|
|
383
|
+
return node.to_s unless Fast.ast_node?(node)
|
|
384
|
+
|
|
385
|
+
node.loc.expression.source
|
|
386
|
+
rescue StandardError
|
|
387
|
+
node.to_s
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def compact_node_source(node)
|
|
391
|
+
source = node_source(node)
|
|
392
|
+
return source unless source.include?("\n")
|
|
393
|
+
|
|
394
|
+
head = source.lines.first.strip
|
|
395
|
+
if head.end_with?('do') || head.include?(' do')
|
|
396
|
+
"#{head} ... end"
|
|
397
|
+
else
|
|
398
|
+
"#{head} ..."
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def print_requires(requires, indent)
|
|
403
|
+
return if requires.empty?
|
|
404
|
+
|
|
405
|
+
formatted = requires.map { |entry| entry.split(' ', 2).last }.join(', ')
|
|
406
|
+
puts "#{indent}requires: #{formatted}"
|
|
407
|
+
puts
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def print_lines(lines, indent)
|
|
411
|
+
return if lines.empty?
|
|
412
|
+
|
|
413
|
+
puts
|
|
414
|
+
lines.each { |line| puts "#{indent}#{line}" }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def print_section(title, lines, indent)
|
|
418
|
+
return if lines.empty?
|
|
419
|
+
|
|
420
|
+
puts
|
|
421
|
+
joined = lines.join(', ')
|
|
422
|
+
if lines.one? || joined.length <= 100
|
|
423
|
+
puts "#{indent}#{title}: #{joined}"
|
|
424
|
+
else
|
|
425
|
+
puts "#{indent}#{title}:"
|
|
426
|
+
lines.each { |line| puts "#{indent} #{line}" }
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def print_methods(methods, indent)
|
|
431
|
+
VISIBILITIES.each do |visibility|
|
|
432
|
+
next if methods[visibility].empty?
|
|
433
|
+
|
|
434
|
+
puts
|
|
435
|
+
puts "#{indent}#{visibility}" unless visibility == :public
|
|
436
|
+
methods[visibility].each { |signature| puts "#{indent}#{' ' unless visibility == :public}#{signature}" }
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
data/lib/fast/version.rb
CHANGED