ffast 0.1.9 → 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,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