ffast 0.2.0 → 0.2.2

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.
data/lib/fast/cli.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'fast'
4
4
  require 'fast/version'
5
+ require 'fast/sql'
5
6
  require 'coderay'
6
7
  require 'optparse'
7
8
  require 'ostruct'
@@ -16,16 +17,44 @@ module Fast
16
17
  # Useful for printing code with syntax highlight.
17
18
  # @param show_sexp [Boolean] prints node expression instead of code
18
19
  # @param colorize [Boolean] skips `CodeRay` processing when false.
19
- def highlight(node, show_sexp: false, colorize: true)
20
+ def highlight(node, show_sexp: false, colorize: true, sql: false)
20
21
  output =
21
22
  if node.respond_to?(:loc) && !show_sexp
22
- node.loc.expression.source
23
+ wrap_source_range(node).source
23
24
  else
24
25
  node
25
26
  end
26
27
  return output unless colorize
27
28
 
28
- CodeRay.scan(output, :ruby).term
29
+ CodeRay.scan(output, sql ? :sql : :ruby).term
30
+ end
31
+
32
+ # Fixes initial spaces to print the line since the beginning
33
+ # and fixes end of the expression including heredoc strings.
34
+ def wrap_source_range(node)
35
+ expression = node.loc.expression
36
+ Parser::Source::Range.new(
37
+ expression.source_buffer,
38
+ first_position_from_expression(node),
39
+ last_position_from_expression(node) || expression.end_pos
40
+ )
41
+ end
42
+
43
+ # If a method call contains a heredoc, it should print the STR around it too.
44
+ def last_position_from_expression(node)
45
+ internal_heredoc = node.each_descendant(:str).select { |n| n.loc.respond_to?(:heredoc_end) }
46
+ internal_heredoc.map { |n| n.loc.heredoc_end.end_pos }.max if internal_heredoc.any?
47
+ end
48
+
49
+ # If a node is the first on it's line, print since the beginning of the line
50
+ # to show the proper whitespaces for identing the next lines of the code.
51
+ def first_position_from_expression(node)
52
+ expression = node.loc.expression
53
+ if node.parent && node.parent.loc.expression.line != expression.line
54
+ expression.begin_pos - expression.column
55
+ else
56
+ expression.begin_pos
57
+ end
29
58
  end
30
59
 
31
60
  # Combines {.highlight} with files printing file name in the head with the
@@ -35,12 +64,14 @@ module Fast
35
64
  # @param file [String] Show the file name and result line before content
36
65
  # @param headless [Boolean] Skip printing the file name and line before content
37
66
  # @example
38
- # Fast.highlight(Fast.search(...))
39
- def report(result, show_link: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) # rubocop:disable Metrics/ParameterLists
67
+ # Fast.report(result, file: 'file.rb')
68
+ def report(result, show_link: false, show_permalink: false, show_sexp: false, file: nil, headless: false, bodyless: false, colorize: true) # rubocop:disable Metrics/ParameterLists
40
69
  if file
41
70
  line = result.loc.expression.line if result.is_a?(Parser::AST::Node)
42
71
  if show_link
43
72
  puts(result.link)
73
+ elsif show_permalink
74
+ puts(result.permalink)
44
75
  elsif !headless
45
76
  puts(highlight("# #{file}:#{line}", colorize: colorize))
46
77
  end
@@ -60,6 +91,9 @@ module Fast
60
91
  option_parser.parse! args
61
92
 
62
93
  @files = [*@files].reject { |arg| arg.start_with?('-') }
94
+ @sql ||= @files.any? && @files.all? { |file| file.end_with?('.sql') }
95
+
96
+ require 'fast/sql' if @sql
63
97
  end
64
98
 
65
99
  def option_parser # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -73,15 +107,24 @@ module Fast
73
107
  @show_sexp = true
74
108
  end
75
109
 
76
- opts.on('--link', 'Print link to repository URL instead of code') do
110
+ opts.on('--link', 'Print link to repository URL') do
77
111
  require 'fast/git'
78
112
  @show_link = true
79
113
  end
80
114
 
115
+ opts.on('--permalink', 'Print permalink to repository URL') do
116
+ require 'fast/git'
117
+ @show_permalink = true
118
+ end
119
+
81
120
  opts.on('-p', '--parallel', 'Paralelize search') do
82
121
  @parallel = true
83
122
  end
84
123
 
124
+ opts.on("--sql", "Use SQL instead of Ruby") do
125
+ @sql = true
126
+ end
127
+
85
128
  opts.on('--captures', 'Print only captures of the patterns and skip node results') do
86
129
  @captures = true
87
130
  end
@@ -99,24 +142,18 @@ module Fast
99
142
  require 'pry'
100
143
  end
101
144
 
102
- opts.on('-c', '--code', 'Create a pattern from code example') do
103
- if @pattern
104
- @from_code = true
105
- @pattern = Fast.ast(@pattern).to_sexp
106
- debug 'Expression from AST:', @pattern
107
- end
108
- end
109
-
110
145
  opts.on('-s', '--similar', 'Search for similar code.') do
111
146
  @similar = true
112
- @pattern = Fast.expression_from(Fast.ast(@pattern))
113
- debug "Looking for code similar to #{@pattern}"
114
147
  end
115
148
 
116
149
  opts.on('--no-color', 'Disable color output') do
117
150
  @colorize = false
118
151
  end
119
152
 
153
+ opts.on('--from-code', 'From code') do
154
+ @from_code = true
155
+ end
156
+
120
157
  opts.on_tail('--version', 'Show version') do
121
158
  puts Fast::VERSION
122
159
  exit
@@ -151,6 +188,25 @@ module Fast
151
188
 
152
189
  if @help || @files.empty? && @pattern.nil?
153
190
  puts option_parser.help
191
+ return
192
+ end
193
+
194
+ if @similar
195
+ ast = Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
196
+ @pattern = Fast.expression_from(ast)
197
+ debug "Search similar to #{@pattern}"
198
+ elsif @from_code
199
+ ast = Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
200
+ @pattern = ast.to_sexp
201
+ if @sql
202
+ @pattern.gsub!(/\b-\b/,'_')
203
+ end
204
+ debug "Search from code to #{@pattern}"
205
+ end
206
+
207
+ if @files.empty?
208
+ ast ||= Fast.public_send( @sql ? :parse_sql : :ast, @pattern)
209
+ puts Fast.highlight(ast, show_sexp: @show_sexp, colorize: @colorize, sql: @sql)
154
210
  else
155
211
  search
156
212
  end
@@ -181,7 +237,7 @@ module Fast
181
237
  # @yieldparam [String, Array] with file and respective search results
182
238
  def execute_search(&on_result)
183
239
  Fast.public_send(search_method_name,
184
- expression,
240
+ @pattern,
185
241
  @files,
186
242
  parallel: parallel?,
187
243
  on_result: on_result)
@@ -212,6 +268,7 @@ module Fast
212
268
  Fast.report(result,
213
269
  file: file,
214
270
  show_link: @show_link,
271
+ show_permalink: @show_permalink,
215
272
  show_sexp: @show_sexp,
216
273
  headless: @headless,
217
274
  bodyless: @bodyless,
@@ -222,8 +279,10 @@ module Fast
222
279
  # @param name [String]
223
280
  # @return [Fast::Shortcut]
224
281
  def find_shortcut(name)
225
- require 'fast/shortcut'
226
- Fast.load_fast_files!
282
+ unless defined? Fast::Shortcut
283
+ require 'fast/shortcut'
284
+ Fast.load_fast_files!
285
+ end
227
286
 
228
287
  shortcut = Fast.shortcuts[name] || Fast.shortcuts[name.to_sym]
229
288
  shortcut || exit_shortcut_not_found(name)
data/lib/fast/shortcut.rb CHANGED
@@ -7,7 +7,12 @@ module Fast
7
7
  # 1. Current directory that the command is being runned
8
8
  # 2. Home folder
9
9
  # 3. Using the `FAST_FILE_DIR` variable to set an extra folder
10
- LOOKUP_FAST_FILES_DIRECTORIES = [Dir.pwd, ENV['HOME'], ENV['FAST_FILE_DIR']].freeze
10
+ LOOKUP_FAST_FILES_DIRECTORIES = [
11
+ Dir.pwd,
12
+ ENV['HOME'],
13
+ ENV['FAST_FILE_DIR'],
14
+ File.join(File.dirname(__FILE__), '..', '..')
15
+ ].compact.map(&File.method(:expand_path)).uniq.freeze
11
16
 
12
17
  # Store predefined searches with default paths through shortcuts.
13
18
  # define your Fastfile in you root folder or
@@ -29,7 +34,7 @@ module Fast
29
34
  def fast_files
30
35
  @fast_files ||= LOOKUP_FAST_FILES_DIRECTORIES.compact
31
36
  .map { |dir| File.join(dir, 'Fastfile') }
32
- .select(&File.method(:exists?))
37
+ .select(&File.method(:exist?))
33
38
  end
34
39
 
35
40
  # Loads `Fastfiles` from {.fast_files} list
@@ -60,7 +65,7 @@ module Fast
60
65
  def merge_args(extra_args)
61
66
  all_args = (@args + extra_args).uniq
62
67
  options = all_args.select { |arg| arg.start_with? '-' }
63
- files = extra_args.select(&File.method(:exists?))
68
+ files = extra_args.select(&File.method(:exist?))
64
69
  command = (@args - options - files).first
65
70
 
66
71
  [command, *options, *files]
@@ -0,0 +1,69 @@
1
+ module Fast
2
+ module SQL
3
+ class << self
4
+ # @see Fast::SQLRewriter
5
+ # @return string with the content updated in case the pattern matches.
6
+ def replace(pattern, ast, &replacement)
7
+ sql_rewriter_for(pattern, ast, &replacement).rewrite!
8
+ end
9
+
10
+ # @return [Fast::SQL::Rewriter]
11
+ # @see Fast::Rewriter
12
+ def sql_rewriter_for(pattern, ast, &replacement)
13
+ rewriter = Rewriter.new
14
+ rewriter.ast = ast
15
+ rewriter.search = pattern
16
+ rewriter.replacement = replacement
17
+ rewriter
18
+ end
19
+
20
+ # @return Fast::SQL::Node with the parsed content
21
+ def parse_file(file)
22
+ parse(IO.read(file), buffer_name: file)
23
+ end
24
+
25
+ # Replace a SQL file with the given pattern.
26
+ # Use a replacement code block to change the content.
27
+ # @return nil in case does not update the file
28
+ # @return true in case the file is updated
29
+ # @see Fast::SQL::Rewriter
30
+ def replace_file(pattern, file, &replacement)
31
+ original = IO.read(file)
32
+ ast = parse_file(file)
33
+ content = replace(pattern, ast, &replacement)
34
+ if content != original
35
+ File.open(file, 'w+') { |f| f.print content }
36
+ end
37
+ end
38
+ end
39
+
40
+ # Extends fast rewriter to support SQL
41
+ # @see Fast::Rewriter
42
+ class Rewriter < Fast::Rewriter
43
+
44
+ # @return [Array<Symbol>] with all types that matches
45
+ def types
46
+ ast.type
47
+ end
48
+
49
+ # 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
51
+ # all matching elements.
52
+ # @see Fast.replace
53
+ def replace_on(*types)
54
+ types.map do |type|
55
+ self.instance_exec do
56
+ self.class.define_method :"on_#{ast.type}" do |node|
57
+ # SQL nodes are not being automatically invoked by the rewriter,
58
+ # so we need to match the root node and invoke on matching inner elements.
59
+ node.search(search).each_with_index do |node, i|
60
+ @match_index += 1
61
+ execute_replacement(node, nil)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/fast/sql.rb ADDED
@@ -0,0 +1,167 @@
1
+ require 'pg_query'
2
+ require_relative 'sql/rewriter'
3
+
4
+ module Fast
5
+
6
+ module_function
7
+
8
+ # Shortcut to parse a sql file
9
+ # @example Fast.parse_sql_file('spec/fixtures/sql/select.sql')
10
+ # @return [Fast::Node] the AST representation of the sql statements from a file
11
+ def parse_sql_file(file)
12
+ SQL.parse_file(file)
13
+ end
14
+
15
+ # @return [Fast::SQLRewriter] which can be used to rewrite the SQL
16
+ # @see Fast::SQLRewriter
17
+ def sql_rewriter_for(pattern, ast, &replacement)
18
+ SQL.rewriter_for(pattern, ast, &replacement)
19
+ end
20
+
21
+ # @return string with the sql content updated in case the pattern matches.
22
+ # @see Fast::SQLRewriter
23
+ # @example
24
+ # Fast.replace_sql('ival', Fast.parse_sql('select 1'), &->(node){ replace(node.location.expression, '2') }) # => "select 2"
25
+ def replace_sql(pattern, ast, &replacement)
26
+ SQL.replace(pattern, ast, &replacement)
27
+ end
28
+
29
+ # @return string with the sql content updated in case the pattern matches.
30
+ def replace_sql_file(pattern, file, &replacement)
31
+ SQL.replace_file(pattern, file, &replacement)
32
+ end
33
+
34
+ # @return [Fast::Node] the AST representation of the sql statement
35
+ # @example
36
+ # ast = Fast.parse_sql("select 'hello AST'")
37
+ # => s(:select_stmt,
38
+ # s(:target_list,
39
+ # s(:res_target,
40
+ # s(:val,
41
+ # s(:a_const,
42
+ # s(:val,
43
+ # s(:string,
44
+ # 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.
47
+ # ast.search(:string).first.location.expression
48
+ # => #<Parser::Source::Range (sql) 7...18>
49
+ def parse_sql(statement, buffer_name: "(sql)")
50
+ SQL.parse(statement, buffer_name: buffer_name)
51
+ end
52
+
53
+ # This module contains methods to parse SQL statements and rewrite them.
54
+ # It uses PGQuery to parse the SQL statements.
55
+ # It uses Parser to rewrite the SQL statements.
56
+ # It uses Parser::Source::Map to map the AST nodes to the SQL tokens.
57
+ #
58
+ # @example
59
+ # Fast::SQL.parse("select 1")
60
+ # => s(:select_stmt, s(:target_list, ...
61
+ # @see Fast::SQL::Node
62
+ module SQL
63
+ # The SQL source buffer is a subclass of Parser::Source::Buffer
64
+ # which contains the tokens of the SQL statement.
65
+ # When you call `ast.location.expression` it will return a range
66
+ # which is mapped to the tokens.
67
+ # @example
68
+ # ast = Fast::SQL.parse("select 1")
69
+ # ast.location.expression # => #<Parser::Source::Range (sql) 0...9>
70
+ # ast.location.expression.source_buffer.tokens
71
+ # => [
72
+ # <PgQuery::ScanToken: start: 0, end: 6, token: :SELECT, keyword_kind: :RESERVED_KEYWORD>,
73
+ # <PgQuery::ScanToken: start: 7, end: 8, token: :ICONST, keyword_kind: :NO_KEYWORD>]
74
+ # @see Fast::SQL::Node
75
+ class SourceBuffer < Parser::Source::Buffer
76
+ def tokens
77
+ @tokens ||= PgQuery.scan(source).first.tokens
78
+ end
79
+ end
80
+
81
+ # The SQL node is an AST node with additional tokenization info
82
+ class Node < Fast::Node
83
+
84
+ def first(pattern)
85
+ search(pattern).first
86
+ end
87
+
88
+ def replace(pattern, with=nil, &replacement)
89
+ replacement ||= -> (n) { replace(n.loc.expression, with) }
90
+ if root?
91
+ SQL.replace(pattern, self, &replacement)
92
+ else
93
+ parent.replace(pattern, &replacement)
94
+ end
95
+ end
96
+
97
+ def token
98
+ tokens.find{|e|e.start == location.begin}
99
+ end
100
+
101
+ def tokens
102
+ location.expression.source_buffer.tokens
103
+ end
104
+ end
105
+
106
+ module_function
107
+
108
+ # Parses SQL statements Using PGQuery
109
+ # @see sql_to_h
110
+ def parse(statement, buffer_name: "(sql)")
111
+ return [] if statement.nil?
112
+ source_buffer = SQL::SourceBuffer.new(buffer_name, source: statement)
113
+ tree = PgQuery.parse(statement).tree
114
+ 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
+ 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)
123
+ end.flatten
124
+ stmts.one? ? stmts.first : stmts
125
+ end
126
+
127
+ # Clean up the hash structure returned by PgQuery
128
+ # @arg [Hash] hash the hash representation of the sql statement
129
+ # @return [Hash] the hash representation of the sql statement
130
+ def clean_structure(stmt)
131
+ res_hash = stmt.map do |key, value|
132
+ value = clean_structure(value) if value.is_a?(Hash)
133
+ value = value.map(&Fast::SQL.method(:clean_structure)) if value.is_a?(Array)
134
+ value = nil if [{}, [], "", :SETOP_NONE, :LIMIT_OPTION_DEFAULT, false].include?(value)
135
+ key = key.to_s.tr('-','_').to_sym
136
+ [key, value]
137
+ end
138
+ res_hash.to_h.compact
139
+ end
140
+
141
+ # Transform a sql tree into an AST.
142
+ # Populates the location of the AST nodes with the source map.
143
+ # @arg [Hash] obj the hash representation of the sql statement
144
+ # @return [Array] the AST representation of the sql statement
145
+ def sql_tree_to_ast(obj, source_buffer: nil, source_map: nil)
146
+ recursive = -> (e) { sql_tree_to_ast(e, source_buffer: source_buffer, source_map: source_map.dup) }
147
+ case obj
148
+ when Array
149
+ obj.map(&recursive).flatten.compact
150
+ when Hash
151
+ if (start = obj.delete(:location))
152
+ 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)
155
+ end
156
+ end
157
+ obj.map do |key, value|
158
+ children = [*recursive.call(value)]
159
+ Node.new(key, children, location: source_map)
160
+ end.compact
161
+ else
162
+ obj
163
+ end
164
+ end
165
+ end
166
+ end
167
+
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.0'
4
+ VERSION = '0.2.2'
5
5
  end
data/lib/fast.rb CHANGED
@@ -148,11 +148,22 @@ module Fast
148
148
 
149
149
  # @return [Fast::Node] parsed from file content
150
150
  # caches the content based on the filename.
151
+ # Also, it can parse SQL files.
151
152
  # @example
152
153
  # Fast.ast_from_file("example.rb") # => s(...)
153
154
  def ast_from_file(file)
154
155
  @cache ||= {}
155
- @cache[file] ||= ast(IO.read(file), buffer_name: file)
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
167
  end
157
168
 
158
169
  # Verify if a given AST matches with a specific pattern
@@ -169,7 +180,12 @@ module Fast
169
180
  node = ast_from_file(file)
170
181
  return [] unless node
171
182
 
172
- search pattern, node
183
+ case node
184
+ when Array
185
+ node.map { |n| search(pattern, n) }.flatten.compact
186
+ else
187
+ search pattern, node
188
+ end
173
189
  end
174
190
 
175
191
  # Search with pattern on a directory or multiple files
@@ -228,8 +244,12 @@ module Fast
228
244
  def capture_file(pattern, file)
229
245
  node = ast_from_file(file)
230
246
  return [] unless node
231
-
232
- capture pattern, node
247
+ case node
248
+ when Array
249
+ node.map { |n| capture(pattern, n) }.flatten.compact
250
+ else
251
+ capture pattern, node
252
+ end
233
253
  end
234
254
 
235
255
  # Search recursively into a node and its children.
@@ -241,9 +261,14 @@ module Fast
241
261
  yield node, match if block_given?
242
262
  match != true ? [node, match] : [node]
243
263
  else
244
- node.each_child_node
245
- .flat_map { |child| search(pattern, child, *args) }
246
- .compact.flatten
264
+ case node
265
+ when Array
266
+ node.flat_map { |child| search(pattern, child, *args) }
267
+ else
268
+ node.each_child_node
269
+ .flat_map { |child| search(pattern, child, *args) }
270
+ .compact.flatten
271
+ end
247
272
  end
248
273
  end
249
274
 
@@ -435,6 +460,10 @@ module Fast
435
460
  node.type == expression.to_sym
436
461
  when String
437
462
  node == expression.to_s
463
+ when TrueClass
464
+ expression == :true
465
+ when FalseClass
466
+ expression == :false
438
467
  else
439
468
  node == expression
440
469
  end
data/mkdocs.yml CHANGED
@@ -1,12 +1,18 @@
1
1
  site_name: Fast
2
2
  repo_url: https://github.com/jonatas/fast
3
3
  edit_uri: edit/master/docs/
4
- google_analytics: ['UA-125089529-1', 'auto']
4
+ google_analytics: ['G-YKZDZDNRG2', 'auto']
5
+
5
6
  theme:
6
7
  name: material
7
8
  palette:
8
9
  primary: indigo
9
10
  accent: pink
11
+ logo: assets/logo.png
12
+ favicon: assets/favicon.png
13
+ extra_css:
14
+ - stylesheets/custom.css
15
+
10
16
  markdown_extensions:
11
17
  - admonition
12
18
  - codehilite:
@@ -15,13 +21,16 @@ markdown_extensions:
15
21
  permalink: true
16
22
  nav:
17
23
  - Introduction: index.md
24
+ - Walkthrough: walkthrough.md
18
25
  - Syntax: syntax.md
19
26
  - Command Line: command_line.md
20
27
  - Experiments: experiments.md
21
28
  - Shortcuts: shortcuts.md
29
+ - Git Integration: git.md
22
30
  - Code Similarity: similarity_tutorial.md
23
31
  - Pry Integration: pry-integration.md
24
32
  - Editors' Integration: editors-integration.md
25
33
  - Research: research.md
26
34
  - Ideas: ideas.md
27
35
  - Videos: videos.md
36
+ - SQL Support: sql-support.md