Slide 1

Slide 1 text

Find and Replace Code based on AST Richard Huang @flyerhzm

Slide 2

Slide 2 text

What is AST? Abstract Syntax Tree class RubyKaigi def year 2023 end def location 'Matsumoto' end end s(:class, s(:const, nil, :RubyKaigi), nil, s(:begin, s(:def, :year, s(:args), s(:int, 2023)), s(:def, :location, s(:args), s(:str, "Matsumoto"))))

Slide 3

Slide 3 text

Why based on AST? Precise # match puts puts "hello world" puts"hello world" puts("hello world") puts( "hello world" ) puts\ "hello world" # not match puts 'puts "hello world"' <<~HEREDOC puts "hello world" HEREDOC custom_puts "hello world"

Slide 4

Slide 4 text

Why based on AST? Precise Powerful Find hash pair where key and value are identical and delete the value Find old hash rocket syntax and replace with new hash syntax Find minitest setup methods that do not call super and prepend the super

Slide 5

Slide 5 text

What tools based on AST? Reek Rubocop Rufo

Slide 6

Slide 6 text

Synvert Write snippet code to rewrite your source code https://synvert.net/

Slide 7

Slide 7 text

Original Idea Simplify the process of upgrading a Rails project

Slide 8

Slide 8 text

RubyKaigi 2014 Write ruby code to change ruby code

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

New Idea Give everyone the ability to find and replace code based on AST

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Node Query NQL - css like node query language Node Rules - hash object https://github.com/xinminlabs/node-query-ruby

Slide 14

Slide 14 text

Find debug code puts "hello world" p current_user # NQL '.send[receiver=nil][message IN (puts p)]' # Node Rules { node_type: 'send', receiver: nil, message: { in: %w[puts p] } }

Slide 15

Slide 15 text

Find two string arguments 'slug from title'.gsub(' ', '_') # NQL '.send[message=gsub] [arguments.size=2] [arguments.first=.str] [arguments.last=.str]' # Node Rules { node_type: 'send', message: 'gsub', arguments: { size: 2, first: { node_type: 'str' }, last: { node_type: 'str' } } }

Slide 16

Slide 16 text

Find hash rocket key { :foo => 'bar' } # NQL %q{.pair[key=.sym][key=~/\A:([^'"]+)\z/]} # Node Rules { node_type: 'pair', key: /\A:([^'"]+)\z/ }

Slide 17

Slide 17 text

Find hash pairs where the key and value are identical a: a, c: c, some_method( 1 2 b: b + 2, 3 4 d: d + 4 5 ) 6 # NQL '.pair[key="{{value}}"]' # Node Rules { node_type: 'pair', key: "{{value}}" }

Slide 18

Slide 18 text

Find minitest setup methods that do not call super def setup do_something end class TestMeme < Minitest::Test 1 2 3 4 end 5 # NQL '.class[parent_class=Minitest::Test] .def[name=setup]:not_has(> .super)' # Node Rules { node_type: 'class', parent_class: 'Minitest::Test' } # first step { node_type: 'def', name: 'setup' } # second step children.none? { |node| node.type == 'super' } # third step

Slide 19

Slide 19 text

Find hash value render nothing: true, status: :created render nothing: true 1 2 # NQL '.hash[status_value!=nil]' # Node Rules { node_type: 'hash', status_value: { not: nil } }

Slide 20

Slide 20 text

NQL rex racc

Slide 21

Slide 21 text

Node Mutation Provides a set of APIs to rewrite node source code https://github.com/xinminlabs/node-mutation-ruby

Slide 22

Slide 22 text

replace_with errors[:base] = "author not present" 1 2 errors.add(:base, "author not present") 3 replace_with node, '{{receiver}}.add({{arguments.0}}, {{arguments.1}})'

Slide 23

Slide 23 text

replace_with errors[:base] = "author not present" 1 2 errors.add(:base, "author not present") 3 errors.add(:base, "author not present") errors[:base] = "author not present" 1 2 3 replace_with node, '{{receiver}}.add({{arguments.0}}, {{arguments.1}})'

Slide 24

Slide 24 text

replace class Post < ActiveRecord::Base end 1 2 3 class Post < ApplicationRecord 4 end 5 replace node, :parent_class, with: 'ApplicationRecord'

Slide 25

Slide 25 text

replace class Post < ActiveRecord::Base end 1 2 3 class Post < ApplicationRecord 4 end 5 class Post < ApplicationRecord end class Post < ActiveRecord::Base 1 end 2 3 4 5 replace node, :parent_class, with: 'ApplicationRecord'

Slide 26

Slide 26 text

insert open('http://test.com') 1 2 URI.open('http://test.com') 3 insert node, 'URI.', at: 'beginning'

Slide 27

Slide 27 text

insert open('http://test.com') 1 2 URI.open('http://test.com') 3 URI.open('http://test.com') open('http://test.com') 1 2 3 insert node, 'URI.', at: 'beginning'

Slide 28

Slide 28 text

prepend def setup do_something end class TestMeme < Minitest::Test 1 2 3 4 end 5 6 class TestMeme < Minitest::Test 7 def setup 8 super 9 do_something 10 end 11 end 12 prepend node, 'super'

Slide 29

Slide 29 text

prepend def setup do_something end class TestMeme < Minitest::Test 1 2 3 4 end 5 6 class TestMeme < Minitest::Test 7 def setup 8 super 9 do_something 10 end 11 end 12 def setup super do_something end class TestMeme < Minitest::Test 1 def setup 2 do_something 3 end 4 end 5 6 class TestMeme < Minitest::Test 7 8 9 10 11 end 12 prepend node, 'super'

Slide 30

Slide 30 text

append class TestMeme < Minitest::Test def teardown clean_something end end 1 2 3 4 5 6 class TestMeme < Minitest::Test 7 def teardown 8 clean_something 9 super 10 end 11 end 12 append node, 'super'

Slide 31

Slide 31 text

append class TestMeme < Minitest::Test def teardown clean_something end end 1 2 3 4 5 6 class TestMeme < Minitest::Test 7 def teardown 8 clean_something 9 super 10 end 11 end 12 class TestMeme < Minitest::Test def teardown clean_something super end end class TestMeme < Minitest::Test 1 def teardown 2 clean_something 3 end 4 end 5 6 7 8 9 10 11 12 append node, 'super'

Slide 32

Slide 32 text

delete FactoryBot.create(:user) 1 2 create(:user) 3 delete node, :receiver, :dot

Slide 33

Slide 33 text

delete FactoryBot.create(:user) 1 2 create(:user) 3 create(:user) FactoryBot.create(:user) 1 2 3 delete node, :receiver, :dot

Slide 34

Slide 34 text

remove puts( "hello"\ "world" ) 1 2 3 4 5 # nothing 6 remove node

Slide 35

Slide 35 text

remove puts( "hello"\ "world" ) 1 2 3 4 5 # nothing 6 # nothing puts( 1 "hello"\ 2 "world" 3 ) 4 5 6 remove node

Slide 36

Slide 36 text

Hash node after_commit :add_to_index_later, on: :create 1 2 after_create_commit :add_to_index_later 3 replace node, :message, with: 'after_{{arguments.-1.on_value.to_value}}_commit' delete node, 'arguments.-1.on_pair', and_comma: true

Slide 37

Slide 37 text

Hash node after_commit :add_to_index_later, on: :create 1 2 after_create_commit :add_to_index_later 3 after_create_commit :add_to_index_later after_commit :add_to_index_later, on: :create 1 2 3 replace node, :message, with: 'after_{{arguments.-1.on_value.to_value}}_commit' delete node, 'arguments.-1.on_pair', and_comma: true

Slide 38

Slide 38 text

node_query + node_mutation require 'parser/current' require 'parser_node_ext' require 'node_query' require 'node_mutation' 1 2 3 4 5 source = "foo\nputs foo\n" 6 node = Parser::CurrentRuby.parse(source) 7 query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') 8 mutation = NodeMutation.new(source) 9 query.query_nodes(node).each do |matching_node| 10 mutation.remove(matching_node) 11 end 12 result = mutation.process 13 result.new_source # foo\n 14

Slide 39

Slide 39 text

node_query + node_mutation require 'parser/current' require 'parser_node_ext' require 'node_query' require 'node_mutation' 1 2 3 4 5 source = "foo\nputs foo\n" 6 node = Parser::CurrentRuby.parse(source) 7 query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') 8 mutation = NodeMutation.new(source) 9 query.query_nodes(node).each do |matching_node| 10 mutation.remove(matching_node) 11 end 12 result = mutation.process 13 result.new_source # foo\n 14 source = "foo\nputs foo\n" node = Parser::CurrentRuby.parse(source) query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') query.query_nodes(node).each do |matching_node| end require 'parser/current' 1 require 'parser_node_ext' 2 require 'node_query' 3 require 'node_mutation' 4 5 6 7 8 mutation = NodeMutation.new(source) 9 10 mutation.remove(matching_node) 11 12 result = mutation.process 13 result.new_source # foo\n 14

Slide 40

Slide 40 text

node_query + node_mutation require 'parser/current' require 'parser_node_ext' require 'node_query' require 'node_mutation' 1 2 3 4 5 source = "foo\nputs foo\n" 6 node = Parser::CurrentRuby.parse(source) 7 query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') 8 mutation = NodeMutation.new(source) 9 query.query_nodes(node).each do |matching_node| 10 mutation.remove(matching_node) 11 end 12 result = mutation.process 13 result.new_source # foo\n 14 source = "foo\nputs foo\n" node = Parser::CurrentRuby.parse(source) query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') query.query_nodes(node).each do |matching_node| end require 'parser/current' 1 require 'parser_node_ext' 2 require 'node_query' 3 require 'node_mutation' 4 5 6 7 8 mutation = NodeMutation.new(source) 9 10 mutation.remove(matching_node) 11 12 result = mutation.process 13 result.new_source # foo\n 14 source = "foo\nputs foo\n" mutation = NodeMutation.new(source) mutation.remove(matching_node) result = mutation.process result.new_source # foo\n require 'parser/current' 1 require 'parser_node_ext' 2 require 'node_query' 3 require 'node_mutation' 4 5 6 node = Parser::CurrentRuby.parse(source) 7 query = NodeQuery.new('.send[receiver=nil][message IN (p puts)]') 8 9 query.query_nodes(node).each do |matching_node| 10 11 end 12 13 14

Slide 41

Slide 41 text

Adapter parser syntax_tree (under development)

Slide 42

Slide 42 text

Synvert Synvert::Rewriter.new 'ruby', 'remove_debug_code' do within_files Synvert::ALL_RUBY_FILES do find_node '.send[receiver=nil][message IN (puts p)]' do remove end end end

Slide 43

Slide 43 text

Check dependencies if_ruby '3.0.0' if_gem 'rails', '~> 6.0.0'

Slide 44

Slide 44 text

Manipulate files add_file 'app/models/application_record.rb', <<~EOS class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end EOS remove_file 'config/initializers/secret_token.rb'

Slide 45

Slide 45 text

Add other snippets add_snippet 'minitest', 'assert_empty' add_snippet 'minitest/assert_instance_of' add_snippet '/Users/flyerhzm/.synvert-ruby/lib/minitest/assert_match. add_snippet 'https://github.com/xinminlabs/synvert-snippets-ruby/blob

Slide 46

Slide 46 text

Support View Files (Erb, Haml, Slim) # <%= h user.login %> # => # <%= user.login %> within_files Synvert::RAILS_VIEW_FILES do with_node type: 'send', receiver: nil, message: 'h' do replace_with '{{arguments}}' end end

Slide 47

Slide 47 text

Synvert CLI synvert-ruby --run rspec/use_new_syntax synvert-ruby --run ~/.synvert-ruby/lib/rspec/use_new_syntax.rb synvert-ruby --run https://raw.githubusercontent.com/xinminlabs/synve

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Synvert GUI https://synvert.net/

Slide 50

Slide 50 text

Run Snippet

Slide 51

Slide 51 text

Automatically generate snippet

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

List Snippets

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

Use Synvert without writing any code Demo

Slide 56

Slide 56 text

Compare with Rubocop

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

Synvert::Rewriter.new 'ruby', 'perfer_not_nil' do within_files Synvert::ALL_RUBY_FILES do find_node '.send[message=!=][arguments.size=1][arguments.0=nil]' do replace_with '!{{receiver}}.nil?' end end end

Slide 59

Slide 59 text

Synvert VSCode Extension https://synvert.net/

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Synvert Web (Playground) https://playground.synvert.net/

Slide 62

Slide 62 text

Synvert Javascript new Synvert.Rewriter("typescript", "array-type", () => { configure({ parser: Synvert.Parser.TYPESCRIPT }); // const z: Array = ['a', 'b']; // => // const z: (string|number)[] = ['a', 'b']; withinFiles(Synvert.ALL_TS_FILES, function () { findNode( `.TypeReference [typeName.escapedText=Array] [typeArguments.0=.UnionType]`, () => { replaceWith("({{typeArguments}})[]"); } );

Slide 63

Slide 63 text

Video Tutorials https://synvert.substack.com/

Slide 64

Slide 64 text

Contact Us https://synvert.net/contact_us

Slide 65

Slide 65 text

Thank You!

Slide 66

Slide 66 text

No content