Class: Parser::AST::Node
- Inherits:
-
Object
- Object
- Parser::AST::Node
- Defined in:
- lib/synvert/core/node_ext.rb
Overview
Extend Parser::AST::Node. https://github.com/whitequark/parser/blob/master/lib/parser/ast/node.rb
Rules
Synvert compares ast nodes with key / value pairs, each ast node has multiple attributes, e.g. receiver
, message
and arguments
, it matches only when all of key / value pairs match.
type: ‘send’, message: :include, arguments: [‘FactoryGirl::Syntax::Methods’]
Synvert does comparison based on the value type
-
if value is a symbol, then compares ast node value as symbol, e.g. message: :include
-
if value is a string, then compares ast node original source code, e.g. name: ‘Synvert::Application’
-
if value is a regexp, then compares ast node original source code, e.g. message: /find_all_by_/
-
if value is an array, then compares each ast node, e.g. arguments: [‘FactoryGirl::Syntax::Methods’]
-
if value is nil, then check if ast node is nil, e.g. arguments: [nil]
-
if value is true or false, then check if ast node is :true or :false, e.g. arguments: [false]
-
if value is ast, then compare ast node directly, e.g. to_ast: Parser::CurrentRuby.parse(“self.class.serialized_attributes”)
It can also compare nested key / value pairs, like
type: ‘send’, receiver: { type: ‘send’, receiver: { type: ‘send’, message: ‘config’ }, message: ‘active_record’ }, message: ‘identity_map=’
Source Code to Ast Node https://synvert-playground.xinminlabs.com/ruby
Constant Summary collapse
- TYPE_CHILDREN =
{ and: %i[left_value right_value], arg: %i[name], begin: %i[body], block: %i[caller arguments body], blockarg: %i[name], const: %i[parent_const name], class: %i[name parent_class body], csend: %i[receiver message arguments], cvasgn: %i[left_value right_value], cvar: %i[name], def: %i[name arguments body], definded?: %i[arguments], defs: %i[self name arguments body], hash: %i[pairs], ivasgn: %i[left_value right_value], ivar: %i[name], lvar: %i[name], lvasgn: %i[left_value right_value], masgn: %i[left_value right_value], module: %i[name body], or: %i[left_value right_value], or_asgn: %i[left_value right_value], pair: %i[key value], restarg: %i[name], send: %i[receiver message arguments], super: %i[arguments], zsuper: %i[] }
Instance Method Summary collapse
-
#arguments ⇒ Array<Parser::AST::Node>
Get arguments of node.
-
#body ⇒ Array<Parser::AST::Node>
Get body of node.
-
#child_node_by_name(child_name) ⇒ Parser::AST::Node
Get child node by the name.
-
#child_node_range(child_name) ⇒ Parser::Source::Range
Get the source range of child node.
-
#column ⇒ Integer
Get the column of node.
-
#condition ⇒ Parser::AST::Node
Get condition of node.
-
#debug_info ⇒ String
Return the debug info.
-
#filename ⇒ String
Get the file name of node.
-
#hash_value(key) ⇒ Parser::AST::Node
Get :hash value node according to specified key.
-
#initialize(type, children = [], properties = {}) ⇒ Node
constructor
Initialize a Node.
-
#key?(key) ⇒ Boolean
Check if :hash node contains specified key.
-
#keys ⇒ Array<Parser::AST::Node>
Get keys of :hash node.
-
#left_value ⇒ Parser::AST::Node
Return the left value of node.
-
#line ⇒ Integer
Get the line of node.
-
#match?(rules) ⇒ Boolean
Match node with rules.
-
#method_missing(method_name, *args, &block) ⇒ Object
Respond key value and source for hash node, e.g.
-
#parent ⇒ Parser::AST::Node
Get the parent node.
-
#parent=(node) ⇒ Object
Set the parent node.
-
#recursive_children {|child| ... } ⇒ Object
Recursively iterate all child nodes of node.
- #respond_to_missing?(method_name, *args) ⇒ Boolean
-
#rewritten_source(code) ⇒ String
Get rewritten source code.
-
#siblings ⇒ Array<Parser::AST::Node>
Get the sibling nodes.
-
#strip_curly_braces ⇒ String
Strip curly braces for hash.
-
#to_hash ⇒ Object
Convert node to a hash, so that it can be converted to a json.
-
#to_lambda_literal ⇒ String
Convert lambda {} to -> {}.
-
#to_single_quote ⇒ String
Get single quote string.
-
#to_source ⇒ String
Get the source code of node.
-
#to_string ⇒ String
Convert symbol to string.
-
#to_symbol ⇒ String
Convert string to symbol.
-
#to_value ⇒ Object
Return the exact value of node.
-
#values ⇒ Array<Parser::AST::Node>
Get values of :hash node.
-
#wrap_curly_braces ⇒ String
Wrap curly braces for hash.
Constructor Details
#initialize(type, children = [], properties = {}) ⇒ Node
Initialize a Node.
It extends Parser::AST::Node and set parent for its child nodes.
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/synvert/core/node_ext.rb', line 65 def initialize(type, children = [], properties = {}) @mutable_attributes = {} super # children could be nil for s(:array) Array(children).each do |child_node| if child_node.is_a?(Parser::AST::Node) child_node.parent = self end end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_name, *args, &block) ⇒ Object
Respond key value and source for hash node, e.g.
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/synvert/core/node_ext.rb', line 278 def method_missing(method_name, *args, &block) if :args == type && children.respond_to?(method_name) return children.send(method_name, *args, &block) elsif :hash == type && method_name.to_s.include?('_value') key = method_name.to_s.sub('_value', '') return hash_value(key.to_sym)&.to_value if key?(key.to_sym) return hash_value(key.to_s)&.to_value if key?(key.to_s) return nil elsif :hash == type && method_name.to_s.include?('_source') key = method_name.to_s.sub('_source', '') return hash_value(key.to_sym)&.to_source if key?(key.to_sym) return hash_value(key.to_s)&.to_source if key?(key.to_s) return nil end super end |
Instance Method Details
#arguments ⇒ Array<Parser::AST::Node>
Get arguments of node. It supports :block, :csend, :def, :defined?, :defs and :send nodes.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/synvert/core/node_ext.rb', line 130 def arguments case type when :def, :block children[1].children when :defs children[2].children when :send, :csend children[2..-1] when :defined? children else raise Synvert::Core::MethodNotSupported, "arguments is not handled for #{debug_info}" end end |
#body ⇒ Array<Parser::AST::Node>
Get body of node. It supports :begin, :block, :class, :def, :defs and :module node.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/synvert/core/node_ext.rb', line 152 def body case type when :begin children when :def, :block, :class, :module return [] if children[2].nil? :begin == children[2].type ? children[2].body : children[2..-1] when :defs return [] if children[3].nil? :begin == children[3].type ? children[3].body : children[3..-1] else raise Synvert::Core::MethodNotSupported, "body is not handled for #{debug_info}" end end |
#child_node_by_name(child_name) ⇒ Parser::AST::Node
Get child node by the name.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/synvert/core/node_ext.rb', line 357 def child_node_by_name(child_name) direct_child_name, nested_child_name = child_name.to_s.split('.', 2) if respond_to?(direct_child_name) child_node = send(direct_child_name) return child_node.child_node_by_name(nested_child_name) if nested_child_name return nil if child_node.nil? return child_node if child_node.is_a?(Parser::AST::Node) return child_node end raise Synvert::Core::MethodNotSupported, "child_node_by_name is not handled for #{debug_info}, child_name: #{child_name}" end |
#child_node_range(child_name) ⇒ Parser::Source::Range
Get the source range of child node.
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 |
# File 'lib/synvert/core/node_ext.rb', line 379 def child_node_range(child_name) case [type, child_name.to_sym] when %i[block pipes], %i[def parentheses], %i[defs parentheses] Parser::Source::Range.new( '(string)', arguments.first.loc.expression.begin_pos - 1, arguments.last.loc.expression.end_pos + 1 ) when %i[block arguments], %i[def arguments], %i[defs arguments] Parser::Source::Range.new( '(string)', arguments.first.loc.expression.begin_pos, arguments.last.loc.expression.end_pos ) when %i[class name], %i[def name], %i[defs name] loc.name when %i[defs dot] loc.operator when %i[defs self] Parser::Source::Range.new('(string)', loc.operator.begin_pos - 'self'.length, loc.operator.begin_pos) when %i[send dot], %i[csend dot] loc.dot when %i[send message], %i[csend message] if loc.operator Parser::Source::Range.new('(string)', loc.selector.begin_pos, loc.operator.end_pos) else loc.selector end when %i[send parentheses], %i[csend parentheses] if loc.begin && loc.end Parser::Source::Range.new('(string)', loc.begin.begin_pos, loc.end.end_pos) end else direct_child_name, nested_child_name = child_name.to_s.split('.', 2) if respond_to?(direct_child_name) child_node = send(direct_child_name) return child_node.child_node_range(nested_child_name) if nested_child_name return nil if child_node.nil? if child_node.is_a?(Parser::AST::Node) return( Parser::Source::Range.new( '(string)', child_node.loc.expression.begin_pos, child_node.loc.expression.end_pos ) ) end # arguments return nil if child_node.empty? return( Parser::Source::Range.new( '(string)', child_node.first.loc.expression.begin_pos, child_node.last.loc.expression.end_pos ) ) end raise Synvert::Core::MethodNotSupported, "child_node_range is not handled for #{debug_info}, child_name: #{child_name}" end end |
#column ⇒ Integer
Get the column of node.
342 343 344 |
# File 'lib/synvert/core/node_ext.rb', line 342 def column loc.expression.column end |
#condition ⇒ Parser::AST::Node
Get condition of node. It supports :if node.
176 177 178 179 180 181 182 |
# File 'lib/synvert/core/node_ext.rb', line 176 def condition if :if == type children[0] else raise Synvert::Core::MethodNotSupported, "condition is not handled for #{debug_info}" end end |
#debug_info ⇒ String
Return the debug info.
315 316 317 318 319 320 321 322 323 |
# File 'lib/synvert/core/node_ext.rb', line 315 def debug_info "\n" + [ "file: #{loc.expression.source_buffer.name}", "line: #{loc.expression.line}", "source: #{to_source}", "node: #{inspect}" ].join("\n") end |
#filename ⇒ String
Get the file name of node.
328 329 330 |
# File 'lib/synvert/core/node_ext.rb', line 328 def filename loc.expression&.source_buffer.name end |
#hash_value(key) ⇒ Parser::AST::Node
Get :hash value node according to specified key.
234 235 236 237 238 239 240 241 |
# File 'lib/synvert/core/node_ext.rb', line 234 def hash_value(key) if :hash == type value_node = children.find { |pair_node| pair_node.key.to_value == key } value_node&.value else raise Synvert::Core::MethodNotSupported, "hash_value is not handled for #{debug_info}" end end |
#key?(key) ⇒ Boolean
Check if :hash node contains specified key.
219 220 221 222 223 224 225 |
# File 'lib/synvert/core/node_ext.rb', line 219 def key?(key) if :hash == type children.any? { |pair_node| pair_node.key.to_value == key } else raise Synvert::Core::MethodNotSupported, "key? is not handled for #{debug_info}" end end |
#keys ⇒ Array<Parser::AST::Node>
Get keys of :hash node.
190 191 192 193 194 195 196 |
# File 'lib/synvert/core/node_ext.rb', line 190 def keys if :hash == type children.map { |child| child.children[0] } else raise Synvert::Core::MethodNotSupported, "keys is not handled for #{debug_info}" end end |
#left_value ⇒ Parser::AST::Node
Return the left value of node. It supports :and, :cvagn, :lvasgn, :masgn, :or and :or_asgn nodes.
114 115 116 117 118 119 120 121 |
# File 'lib/synvert/core/node_ext.rb', line 114 def left_value return children[0].children[0] if type == :or_asgn index = TYPE_CHILDREN[type]&.index(:left_value) return children[index] if index raise Synvert::Core::MethodNotSupported, "#{left_value} is not handled for #{debug_info}" end |
#line ⇒ Integer
Get the line of node.
349 350 351 |
# File 'lib/synvert/core/node_ext.rb', line 349 def line loc.expression.line end |
#match?(rules) ⇒ Boolean
Match node with rules. It provides some additional keywords to match rules, any
, contain
, not
, in
, not_in
, gt
, gte
, lt
, lte
.
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'lib/synvert/core/node_ext.rb', line 468 def match?(rules) keywords = %i[any contain not in not_in gt gte lt lte] flat_hash(rules).keys.all? do |multi_keys| last_key = multi_keys.last actual = keywords.include?(last_key) ? actual_value(multi_keys[0...-1]) : actual_value(multi_keys) expected = expected_value(rules, multi_keys) case last_key when :any, :contain actual.any? { |actual_value| match_value?(actual_value, expected) } when :not !match_value?(actual, expected) when :in expected.any? { |expected_value| match_value?(actual, expected_value) } when :not_in expected.all? { |expected_value| !match_value?(actual, expected_value) } when :gt actual > expected when :gte actual >= expected when :lt actual < expected when :lte actual <= expected else match_value?(actual, expected) end end end |
#parent ⇒ Parser::AST::Node
Get the parent node.
78 79 80 |
# File 'lib/synvert/core/node_ext.rb', line 78 def parent @mutable_attributes[:parent] end |
#parent=(node) ⇒ Object
Set the parent node.
84 85 86 |
# File 'lib/synvert/core/node_ext.rb', line 84 def parent=(node) @mutable_attributes[:parent] = node end |
#recursive_children {|child| ... } ⇒ Object
Recursively iterate all child nodes of node.
451 452 453 454 455 456 457 458 |
# File 'lib/synvert/core/node_ext.rb', line 451 def recursive_children(&block) children.each do |child| if child.is_a?(Parser::AST::Node) stop = yield child child.recursive_children(&block) unless stop == :stop end end end |
#respond_to_missing?(method_name, *args) ⇒ Boolean
298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/synvert/core/node_ext.rb', line 298 def respond_to_missing?(method_name, *args) if :args == type && children.respond_to?(method_name) return true elsif :hash == type && method_name.to_s.include?('_value') key = method_name.to_s.sub('_value', '') return true if key?(key.to_sym) || key?(key.to_s) elsif :hash == type && method_name.to_s.include?('_source') key = method_name.to_s.sub('_source', '') return true if key?(key.to_sym) || key?(key.to_s) end super end |
#rewritten_source(code) ⇒ String
Get rewritten source code.
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 |
# File 'lib/synvert/core/node_ext.rb', line 503 def rewritten_source(code) code.gsub(/{{(.*?)}}/m) do old_code = Regexp.last_match(1) if respond_to?(old_code.split('.').first) evaluated = child_node_by_name(old_code) case evaluated when Parser::AST::Node if evaluated.type == :args evaluated.loc.expression.source[1...-1] else evaluated.loc.expression.source end when Array if evaluated.size > 0 file_source = evaluated.first.loc.expression.source_buffer.source source = file_source[evaluated.first.loc.expression.begin_pos...evaluated.last.loc.expression.end_pos] lines = source.split "\n" lines_count = lines.length if lines_count > 1 && lines_count == evaluated.size new_code = [] lines.each_with_index { |line, index| new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1]) } new_code.join("\n") else source end end when String, Symbol, Integer, Float evaluated when NilClass 'nil' else raise Synvert::Core::MethodNotSupported, "rewritten_source is not handled for #{evaluated.inspect}" end else "{{#{old_code}}}" end end end |
#siblings ⇒ Array<Parser::AST::Node>
Get the sibling nodes.
90 91 92 93 |
# File 'lib/synvert/core/node_ext.rb', line 90 def siblings index = parent.children.index(self) parent.children[index + 1..] end |
#strip_curly_braces ⇒ String
Strip curly braces for hash.
549 550 551 552 553 |
# File 'lib/synvert/core/node_ext.rb', line 549 def strip_curly_braces return to_source unless type == :hash to_source.sub(/^{(.*)}$/) { Regexp.last_match(1).strip } end |
#to_hash ⇒ Object
Convert node to a hash, so that it can be converted to a json.
620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 |
# File 'lib/synvert/core/node_ext.rb', line 620 def to_hash result = { type: type } if TYPE_CHILDREN[type] TYPE_CHILDREN[type].each do |key| value = send(key) result[key] = case value when Array value.map { |v| v.respond_to?(:to_hash) ? v.to_hash : v } when Parser::AST::Node value.to_hash else value end end else result[:children] = children.map { |c| c.respond_to?(:to_hash) ? c.to_hash : c } end result end |
#to_lambda_literal ⇒ String
Convert lambda {} to -> {}
604 605 606 607 608 609 610 611 612 613 614 615 616 617 |
# File 'lib/synvert/core/node_ext.rb', line 604 def to_lambda_literal if type == :block && caller.type == :send && caller.receiver.nil? && caller. == :lambda new_source = to_source if arguments.size > 1 new_source = new_source[0...arguments.first.loc.expression.begin_pos - 2] + new_source[arguments.last.loc.expression.end_pos + 1..-1] new_source = new_source.sub('lambda', "->(#{arguments.map(&:to_source).join(', ')})") else new_source = new_source.sub('lambda', '->') end new_source else to_source end end |
#to_single_quote ⇒ String
Get single quote string.
571 572 573 574 575 |
# File 'lib/synvert/core/node_ext.rb', line 571 def to_single_quote return to_source unless type == :str "'#{to_value}'" end |
#to_source ⇒ String
Get the source code of node.
335 336 337 |
# File 'lib/synvert/core/node_ext.rb', line 335 def to_source loc.expression&.source end |
#to_string ⇒ String
Convert symbol to string.
593 594 595 596 597 |
# File 'lib/synvert/core/node_ext.rb', line 593 def to_string return to_source unless type == :sym to_value.to_s end |
#to_symbol ⇒ String
Convert string to symbol.
582 583 584 585 586 |
# File 'lib/synvert/core/node_ext.rb', line 582 def to_symbol return to_source unless type == :str ":#{to_value}" end |
#to_value ⇒ Object
Return the exact value of node. It supports :array, :begin, :erange, :false, :float, :irange, :int, :str, :sym and :true nodes.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/synvert/core/node_ext.rb', line 250 def to_value case type when :int, :float, :str, :sym children.last when :true true when :false false when :nil nil when :array children.map(&:to_value) when :irange (children.first.to_value..children.last.to_value) when :erange (children.first.to_value...children.last.to_value) when :begin children.first.to_value else self end end |
#values ⇒ Array<Parser::AST::Node>
Get values of :hash node.
204 205 206 207 208 209 210 |
# File 'lib/synvert/core/node_ext.rb', line 204 def values if :hash == type children.map { |child| child.children[1] } else raise Synvert::Core::MethodNotSupported, "keys is not handled for #{debug_info}" end end |
#wrap_curly_braces ⇒ String
Wrap curly braces for hash.
560 561 562 563 564 |
# File 'lib/synvert/core/node_ext.rb', line 560 def wrap_curly_braces return to_source unless type == :hash "{ #{to_source} }" end |