ffast 0.1.9 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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,13 +64,19 @@ 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_sexp: false, file: nil, headless: false, colorize: true)
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
- puts(highlight("# #{file}:#{line}", colorize: colorize)) unless headless
71
+ if show_link
72
+ puts(result.link)
73
+ elsif show_permalink
74
+ puts(result.permalink)
75
+ elsif !headless
76
+ puts(highlight("# #{file}:#{line}", colorize: colorize))
77
+ end
43
78
  end
44
- puts highlight(result, show_sexp: show_sexp, colorize: colorize)
79
+ puts(highlight(result, show_sexp: show_sexp, colorize: colorize)) unless bodyless
45
80
  end
46
81
 
47
82
  # Command Line Interface for Fast
@@ -56,6 +91,9 @@ module Fast
56
91
  option_parser.parse! args
57
92
 
58
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
59
97
  end
60
98
 
61
99
  def option_parser # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -69,10 +107,24 @@ module Fast
69
107
  @show_sexp = true
70
108
  end
71
109
 
110
+ opts.on('--link', 'Print link to repository URL') do
111
+ require 'fast/git'
112
+ @show_link = true
113
+ end
114
+
115
+ opts.on('--permalink', 'Print permalink to repository URL') do
116
+ require 'fast/git'
117
+ @show_permalink = true
118
+ end
119
+
72
120
  opts.on('-p', '--parallel', 'Paralelize search') do
73
121
  @parallel = true
74
122
  end
75
123
 
124
+ opts.on("--sql", "Use SQL instead of Ruby") do
125
+ @sql = true
126
+ end
127
+
76
128
  opts.on('--captures', 'Print only captures of the patterns and skip node results') do
77
129
  @captures = true
78
130
  end
@@ -81,29 +133,27 @@ module Fast
81
133
  @headless = true
82
134
  end
83
135
 
136
+ opts.on('--bodyless', 'Print results without the code details') do
137
+ @bodyless = true
138
+ end
139
+
84
140
  opts.on('--pry', 'Jump into a pry session with results') do
85
141
  @pry = true
86
142
  require 'pry'
87
143
  end
88
144
 
89
- opts.on('-c', '--code', 'Create a pattern from code example') do
90
- if @pattern
91
- @from_code = true
92
- @pattern = Fast.ast(@pattern).to_sexp
93
- debug 'Expression from AST:', @pattern
94
- end
95
- end
96
-
97
145
  opts.on('-s', '--similar', 'Search for similar code.') do
98
146
  @similar = true
99
- @pattern = Fast.expression_from(Fast.ast(@pattern))
100
- debug "Looking for code similar to #{@pattern}"
101
147
  end
102
148
 
103
149
  opts.on('--no-color', 'Disable color output') do
104
150
  @colorize = false
105
151
  end
106
152
 
153
+ opts.on('--from-code', 'From code') do
154
+ @from_code = true
155
+ end
156
+
107
157
  opts.on_tail('--version', 'Show version') do
108
158
  puts Fast::VERSION
109
159
  exit
@@ -116,12 +166,13 @@ module Fast
116
166
  end
117
167
 
118
168
  def replace_args_with_shortcut(args)
119
- shortcut = find_shortcut args.first[1..-1]
169
+ shortcut = find_shortcut args.first[1..]
170
+
120
171
  if shortcut.single_run_with_block?
121
172
  shortcut.run
122
173
  exit
123
174
  else
124
- args.one? ? shortcut.args : shortcut.merge_args(args[1..-1])
175
+ args.one? ? shortcut.args : shortcut.merge_args(args[1..])
125
176
  end
126
177
  end
127
178
 
@@ -137,6 +188,25 @@ module Fast
137
188
 
138
189
  if @help || @files.empty? && @pattern.nil?
139
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)
140
210
  else
141
211
  search
142
212
  end
@@ -167,7 +237,7 @@ module Fast
167
237
  # @yieldparam [String, Array] with file and respective search results
168
238
  def execute_search(&on_result)
169
239
  Fast.public_send(search_method_name,
170
- expression,
240
+ @pattern,
171
241
  @files,
172
242
  parallel: parallel?,
173
243
  on_result: on_result)
@@ -195,18 +265,26 @@ module Fast
195
265
  # Report results using the actual options binded from command line.
196
266
  # @see Fast.report
197
267
  def report(file, result)
198
- Fast.report(result, file: file, show_sexp: @show_sexp, headless: @headless, colorize: @colorize)
268
+ Fast.report(result,
269
+ file: file,
270
+ show_link: @show_link,
271
+ show_permalink: @show_permalink,
272
+ show_sexp: @show_sexp,
273
+ headless: @headless,
274
+ bodyless: @bodyless,
275
+ colorize: @colorize)
199
276
  end
200
277
 
201
278
  # Find shortcut by name. Preloads all `Fastfiles` before start.
202
279
  # @param name [String]
203
280
  # @return [Fast::Shortcut]
204
281
  def find_shortcut(name)
205
- require 'fast/shortcut'
206
- Fast.load_fast_files!
282
+ unless defined? Fast::Shortcut
283
+ require 'fast/shortcut'
284
+ Fast.load_fast_files!
285
+ end
207
286
 
208
287
  shortcut = Fast.shortcuts[name] || Fast.shortcuts[name.to_sym]
209
-
210
288
  shortcut || exit_shortcut_not_found(name)
211
289
  end
212
290
 
@@ -290,7 +290,7 @@ module Fast
290
290
  Fast.search(experiment.expression, @ast) || []
291
291
  end
292
292
 
293
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
293
+ # rubocop:disable Metrics/MethodLength
294
294
  #
295
295
  # Execute partial replacements generating new file with the
296
296
  # content replaced.
@@ -312,7 +312,6 @@ module Fast
312
312
  new_content
313
313
  end
314
314
 
315
- # rubocop:enable Metrics/AbcSize
316
315
  # rubocop:enable Metrics/MethodLength
317
316
 
318
317
  # Write new file name depending on the combination
data/lib/fast/git.rb ADDED
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Git plugin for Fast::Node.
4
+ # It allows to easily access metadata from current file.
5
+ module Fast
6
+ # This is not required by default, so to use it, you should require it first.
7
+ #
8
+ # @example
9
+ # require 'fast/git'
10
+ # Fast.ast_from_file('lib/fast.rb').git_log.first.author.name # => "Jonatas Davi Paganini"
11
+ class Node < Astrolabe::Node
12
+ # @return [Git::Base] from current directory
13
+ def git
14
+ require 'git' unless defined? Git
15
+ Git.open('.')
16
+ end
17
+
18
+ # @return [Git::Object::Blob] from current #buffer_name
19
+ def git_blob
20
+ return unless from_file?
21
+
22
+ git.gblob(buffer_name)
23
+ end
24
+
25
+ # @return [Git::Log] from the current #git_blob
26
+ # buffer-name
27
+ def git_log
28
+ git_blob.log
29
+ end
30
+
31
+ # @return [Git::Object::Commit]
32
+ def last_commit
33
+ git_log.first
34
+ end
35
+
36
+ # @return [String] with last commit SHA
37
+ def sha
38
+ last_commit.sha
39
+ end
40
+
41
+ # @return [String] with remote URL
42
+ def remote_url
43
+ git.remote.url
44
+ end
45
+
46
+ # Given #remote_url is "git@github.com:namespace/project.git"
47
+ # Or #remote_url is "https://github.com/namespace/project.git"
48
+ # @return [String] "https://github.com/namespace/project"
49
+ def project_url
50
+ return remote_url.gsub(/\.git$/, '') if remote_url.start_with?('https')
51
+
52
+ remote_url
53
+ .gsub('git@', 'https://')
54
+ .gsub(/:(\w)/, '/\\1')
55
+ .gsub(/\.git$/, '')
56
+ end
57
+
58
+ def file
59
+ buffer_name.gsub("#{Dir.pwd}/", '')
60
+ end
61
+
62
+ # @return
63
+ def line_range
64
+ lines.map { |l| "L#{l}" }.join('-')
65
+ end
66
+
67
+ # @return [Array] with lines range
68
+ def lines
69
+ exp = loc.expression
70
+ first_line = exp.first_line
71
+ last_line = exp.last_line
72
+ [first_line, last_line].uniq
73
+ end
74
+
75
+ # @return [Integer] lines of code from current block
76
+ def lines_of_code
77
+ lines.last - lines.first + 1
78
+ end
79
+
80
+ # @return [String] a markdown link with #md_link_description and #link
81
+ def md_link(text = md_link_description)
82
+ "[#{text}](#{link})"
83
+ end
84
+
85
+ # @return [String] with the source cutting arguments from method calls to be
86
+ # able to create a markdown link without parens.
87
+ def md_link_description
88
+ source[/([^\r\(]+)\(/, 1] || source
89
+ end
90
+
91
+ # @return [String] with formatted repositorym link
92
+ def link
93
+ "#{project_url}/blob/master/#{buffer_name}##{line_range}"
94
+ end
95
+
96
+ # @return [String] with permanent link to the actual commit
97
+ def permalink
98
+ "#{project_url}/blob/#{sha}/#{buffer_name}##{line_range}"
99
+ end
100
+ end
101
+ end
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
@@ -54,19 +59,16 @@ module Fast
54
59
  @block && @args.nil?
55
60
  end
56
61
 
57
- def options
58
- @args.select { |arg| arg.start_with? '-' }
59
- end
60
-
61
- def params
62
- @args - options
63
- end
64
-
65
62
  # Merge extra arguments from input returning a new arguments array keeping
66
63
  # the options from previous alias and replacing the files with the
67
64
  # @param [Array] extra_args
68
65
  def merge_args(extra_args)
69
- [params[0], *options, *extra_args.select(&File.method(:exists?))]
66
+ all_args = (@args + extra_args).uniq
67
+ options = all_args.select { |arg| arg.start_with? '-' }
68
+ files = extra_args.select(&File.method(:exist?))
69
+ command = (@args - options - files).first
70
+
71
+ [command, *options, *files]
70
72
  end
71
73
 
72
74
  # If the shortcut was defined with a single block and no extra arguments, it
@@ -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.1.9'
4
+ VERSION = '0.2.2'
5
5
  end