ffast 0.0.7 → 0.0.8
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.
- checksums.yaml +4 -4
- data/bin/fast-experiment +1 -0
- data/lib/fast.rb +3 -233
- data/lib/fast/cli.rb +1 -3
- data/lib/fast/experiment.rb +249 -0
- data/lib/fast/version.rb +1 -1
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5e62b5be6bf84eabaa2ed601696164c248a6969cc504343f636b1d487bb4fbf2
         | 
| 4 | 
            +
              data.tar.gz: a86d3f27396de8cb9aa097c02d1baf19c39ad7090f03b5894e1eddb087150f6b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 4a2a7db73282e36fd8ece6d9d78b55eb52e98227e5be77d77058a82ae97af634e94de7dd5f98640a844739956f5e8c5bf9f2fd91e88971d21da6a762cf69fdbb
         | 
| 7 | 
            +
              data.tar.gz: 635bd2bb2a9e26975731fa4096f7980281ef929d2ae13d54f8f4897e8695d6e824deb15ed4cea6e6df9114191e99e210510a5878dc703910b711e1a17c446a68
         | 
    
        data/bin/fast-experiment
    CHANGED
    
    
    
        data/lib/fast.rb
    CHANGED
    
    | @@ -69,7 +69,7 @@ module Fast | |
| 69 69 | 
             
                  Matcher.new(ast, search, *args).match?
         | 
| 70 70 | 
             
                end
         | 
| 71 71 |  | 
| 72 | 
            -
                def replace(ast, search, replacement)
         | 
| 72 | 
            +
                def replace(ast, search, &replacement)
         | 
| 73 73 | 
             
                  buffer = Parser::Source::Buffer.new('replacement')
         | 
| 74 74 | 
             
                  buffer.source = ast.loc.expression.source
         | 
| 75 75 | 
             
                  to_replace = search(ast, search)
         | 
| @@ -82,9 +82,9 @@ module Fast | |
| 82 82 | 
             
                  rewriter.rewrite(buffer, ast)
         | 
| 83 83 | 
             
                end
         | 
| 84 84 |  | 
| 85 | 
            -
                def replace_file(file, search, replacement)
         | 
| 85 | 
            +
                def replace_file(file, search, &replacement)
         | 
| 86 86 | 
             
                  ast = ast_from_file(file)
         | 
| 87 | 
            -
                  replace(ast, search, replacement)
         | 
| 87 | 
            +
                  replace(ast, search, &replacement)
         | 
| 88 88 | 
             
                end
         | 
| 89 89 |  | 
| 90 90 | 
             
                def search_file(pattern, file)
         | 
| @@ -152,13 +152,6 @@ module Fast | |
| 152 152 | 
             
                def expression(string)
         | 
| 153 153 | 
             
                  ExpressionParser.new(string).parse
         | 
| 154 154 | 
             
                end
         | 
| 155 | 
            -
             | 
| 156 | 
            -
                def experiment(name, &block)
         | 
| 157 | 
            -
                  @experiments ||= {}
         | 
| 158 | 
            -
                  @experiments[name] = Experiment.new(name, &block)
         | 
| 159 | 
            -
                end
         | 
| 160 | 
            -
             | 
| 161 | 
            -
                attr_reader :experiments
         | 
| 162 155 | 
             
                attr_accessor :debugging
         | 
| 163 156 |  | 
| 164 157 | 
             
                def debug
         | 
| @@ -540,7 +533,6 @@ module Fast | |
| 540 533 | 
             
                  end
         | 
| 541 534 | 
             
                end
         | 
| 542 535 |  | 
| 543 | 
            -
                # rubocop:disable Metrics/AbcSize
         | 
| 544 536 | 
             
                def match?(ast = @ast, fast = @fast)
         | 
| 545 537 | 
             
                  head, *tail = fast
         | 
| 546 538 | 
             
                  return false unless head.match?(ast)
         | 
| @@ -555,8 +547,6 @@ module Fast | |
| 555 547 | 
             
                  end && find_captures
         | 
| 556 548 | 
             
                end
         | 
| 557 549 |  | 
| 558 | 
            -
                # rubocop:enable Metrics/AbcSize
         | 
| 559 | 
            -
             | 
| 560 550 | 
             
                def prepare_token(token)
         | 
| 561 551 | 
             
                  case token
         | 
| 562 552 | 
             
                  when Fast::FindWithCapture
         | 
| @@ -582,224 +572,4 @@ module Fast | |
| 582 572 | 
             
                  end
         | 
| 583 573 | 
             
                end
         | 
| 584 574 | 
             
              end
         | 
| 585 | 
            -
             | 
| 586 | 
            -
              # You can define experiments and build experimental files to improve some code in
         | 
| 587 | 
            -
              # an automated way. Let's create a hook to check if a `before` or `after` block
         | 
| 588 | 
            -
              # is useless in a specific spec:
         | 
| 589 | 
            -
              #
         | 
| 590 | 
            -
              # ```ruby
         | 
| 591 | 
            -
              # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
         | 
| 592 | 
            -
              #   lookup 'some_spec.rb'
         | 
| 593 | 
            -
              #   search "(block (send nil {before after}))"
         | 
| 594 | 
            -
              #   edit {|node| remove(node.loc.expression) }
         | 
| 595 | 
            -
              #   policy {|new_file| system("bin/spring rspec --fail-fast #{new_file}") }
         | 
| 596 | 
            -
              # end
         | 
| 597 | 
            -
              # ```
         | 
| 598 | 
            -
              class Experiment
         | 
| 599 | 
            -
                attr_writer :files
         | 
| 600 | 
            -
                attr_reader :name, :replacement, :expression, :files_or_folders, :ok_if
         | 
| 601 | 
            -
             | 
| 602 | 
            -
                def initialize(name, &block)
         | 
| 603 | 
            -
                  @name = name
         | 
| 604 | 
            -
                  puts "\nStarting experiment: #{name}"
         | 
| 605 | 
            -
                  instance_exec(&block)
         | 
| 606 | 
            -
                end
         | 
| 607 | 
            -
             | 
| 608 | 
            -
                def run_with(file)
         | 
| 609 | 
            -
                  ExperimentFile.new(file, self).run
         | 
| 610 | 
            -
                end
         | 
| 611 | 
            -
             | 
| 612 | 
            -
                def search(expression)
         | 
| 613 | 
            -
                  @expression = expression
         | 
| 614 | 
            -
                end
         | 
| 615 | 
            -
             | 
| 616 | 
            -
                def edit(&block)
         | 
| 617 | 
            -
                  @replacement = block
         | 
| 618 | 
            -
                end
         | 
| 619 | 
            -
             | 
| 620 | 
            -
                def lookup(files_or_folders)
         | 
| 621 | 
            -
                  @files_or_folders = files_or_folders
         | 
| 622 | 
            -
                end
         | 
| 623 | 
            -
             | 
| 624 | 
            -
                def policy(&block)
         | 
| 625 | 
            -
                  @ok_if = block
         | 
| 626 | 
            -
                end
         | 
| 627 | 
            -
             | 
| 628 | 
            -
                def files
         | 
| 629 | 
            -
                  @files ||= Fast.ruby_files_from(@files_or_folders)
         | 
| 630 | 
            -
                end
         | 
| 631 | 
            -
             | 
| 632 | 
            -
                def run
         | 
| 633 | 
            -
                  files.map(&method(:run_with))
         | 
| 634 | 
            -
                end
         | 
| 635 | 
            -
              end
         | 
| 636 | 
            -
             | 
| 637 | 
            -
              # Suggest possible combinations of occurrences to replace.
         | 
| 638 | 
            -
              class ExperimentCombinations
         | 
| 639 | 
            -
                attr_reader :combinations
         | 
| 640 | 
            -
             | 
| 641 | 
            -
                def initialize(round:, occurrences_count:, ok_experiments:, fail_experiments:)
         | 
| 642 | 
            -
                  @round = round
         | 
| 643 | 
            -
                  @ok_experiments = ok_experiments
         | 
| 644 | 
            -
                  @fail_experiments = fail_experiments
         | 
| 645 | 
            -
                  @occurrences_count = occurrences_count
         | 
| 646 | 
            -
                end
         | 
| 647 | 
            -
             | 
| 648 | 
            -
                def generate_combinations
         | 
| 649 | 
            -
                  case @round
         | 
| 650 | 
            -
                  when 1
         | 
| 651 | 
            -
                    individual_replacements
         | 
| 652 | 
            -
                  when 2
         | 
| 653 | 
            -
                    all_ok_replacements_combined
         | 
| 654 | 
            -
                  else
         | 
| 655 | 
            -
                    ok_replacements_pair_combinations
         | 
| 656 | 
            -
                  end
         | 
| 657 | 
            -
                end
         | 
| 658 | 
            -
             | 
| 659 | 
            -
                # Replace a single occurrence at each iteration and identify which
         | 
| 660 | 
            -
                # individual replacements work.
         | 
| 661 | 
            -
                def individual_replacements
         | 
| 662 | 
            -
                  (1..@occurrences_count).to_a
         | 
| 663 | 
            -
                end
         | 
| 664 | 
            -
             | 
| 665 | 
            -
                # After identifying all individual replacements that work, try combining all
         | 
| 666 | 
            -
                # of them.
         | 
| 667 | 
            -
                def all_ok_replacements_combined
         | 
| 668 | 
            -
                  [@ok_experiments.uniq.sort]
         | 
| 669 | 
            -
                end
         | 
| 670 | 
            -
             | 
| 671 | 
            -
                # Combining all successful individual replacements has failed. Lets divide
         | 
| 672 | 
            -
                # and conquer.
         | 
| 673 | 
            -
                def ok_replacements_pair_combinations
         | 
| 674 | 
            -
                  @ok_experiments
         | 
| 675 | 
            -
                    .combination(2)
         | 
| 676 | 
            -
                    .map { |e| e.flatten.uniq.sort }
         | 
| 677 | 
            -
                    .uniq - @fail_experiments - @ok_experiments
         | 
| 678 | 
            -
                end
         | 
| 679 | 
            -
              end
         | 
| 680 | 
            -
             | 
| 681 | 
            -
              # Encapsulate the join of an Experiment with an specific file.
         | 
| 682 | 
            -
              # This is important to coordinate and regulate multiple experiments in the same file.
         | 
| 683 | 
            -
              # It can track successfull experiments and failures and suggest new combinations to keep replacing the file.
         | 
| 684 | 
            -
              class ExperimentFile
         | 
| 685 | 
            -
                attr_reader :ok_experiments, :fail_experiments, :experiment
         | 
| 686 | 
            -
             | 
| 687 | 
            -
                def initialize(file, experiment)
         | 
| 688 | 
            -
                  @file = file
         | 
| 689 | 
            -
                  @ast = Fast.ast_from_file(file) if file
         | 
| 690 | 
            -
                  @experiment = experiment
         | 
| 691 | 
            -
                  @ok_experiments = []
         | 
| 692 | 
            -
                  @fail_experiments = []
         | 
| 693 | 
            -
                  @round = 0
         | 
| 694 | 
            -
                end
         | 
| 695 | 
            -
             | 
| 696 | 
            -
                def search
         | 
| 697 | 
            -
                  experiment.expression
         | 
| 698 | 
            -
                end
         | 
| 699 | 
            -
             | 
| 700 | 
            -
                def experimental_filename(combination)
         | 
| 701 | 
            -
                  parts = @file.split('/')
         | 
| 702 | 
            -
                  dir = parts[0..-2]
         | 
| 703 | 
            -
                  filename = "experiment_#{[*combination].join('_')}_#{parts[-1]}"
         | 
| 704 | 
            -
                  File.join(*dir, filename)
         | 
| 705 | 
            -
                end
         | 
| 706 | 
            -
             | 
| 707 | 
            -
                def ok_with(combination)
         | 
| 708 | 
            -
                  @ok_experiments << combination
         | 
| 709 | 
            -
                  return unless combination.is_a?(Array)
         | 
| 710 | 
            -
             | 
| 711 | 
            -
                  combination.each do |element|
         | 
| 712 | 
            -
                    @ok_experiments.delete(element)
         | 
| 713 | 
            -
                  end
         | 
| 714 | 
            -
                end
         | 
| 715 | 
            -
             | 
| 716 | 
            -
                def failed_with(combination)
         | 
| 717 | 
            -
                  @fail_experiments << combination
         | 
| 718 | 
            -
                end
         | 
| 719 | 
            -
             | 
| 720 | 
            -
                def search_cases
         | 
| 721 | 
            -
                  Fast.search(@ast, experiment.expression) || []
         | 
| 722 | 
            -
                end
         | 
| 723 | 
            -
             | 
| 724 | 
            -
                # rubocop:disable Metrics/AbcSize
         | 
| 725 | 
            -
                # rubocop:disable Metrics/MethodLength
         | 
| 726 | 
            -
                def partial_replace(*indices)
         | 
| 727 | 
            -
                  replacement = experiment.replacement
         | 
| 728 | 
            -
                  new_content = Fast.replace_file @file, experiment.expression, ->(node, *captures) do # rubocop:disable Style/Lambda
         | 
| 729 | 
            -
                    if indices.nil? || indices.empty? || indices.include?(match_index)
         | 
| 730 | 
            -
                      if replacement.parameters.length == 1
         | 
| 731 | 
            -
                        instance_exec node, &replacement
         | 
| 732 | 
            -
                      else
         | 
| 733 | 
            -
                        instance_exec node, *captures, &replacement
         | 
| 734 | 
            -
                      end
         | 
| 735 | 
            -
                    end
         | 
| 736 | 
            -
                  end
         | 
| 737 | 
            -
                  return unless new_content
         | 
| 738 | 
            -
             | 
| 739 | 
            -
                  write_experiment_file(indices, new_content)
         | 
| 740 | 
            -
                  new_content
         | 
| 741 | 
            -
                end
         | 
| 742 | 
            -
                # rubocop:enable Metrics/AbcSize
         | 
| 743 | 
            -
                # rubocop:enable Metrics/MethodLength
         | 
| 744 | 
            -
             | 
| 745 | 
            -
                def write_experiment_file(index, new_content)
         | 
| 746 | 
            -
                  filename = experimental_filename(index)
         | 
| 747 | 
            -
                  File.open(filename, 'w+') { |f| f.puts new_content }
         | 
| 748 | 
            -
                  filename
         | 
| 749 | 
            -
                end
         | 
| 750 | 
            -
             | 
| 751 | 
            -
                def done!
         | 
| 752 | 
            -
                  count_executed_combinations = @fail_experiments.size + @ok_experiments.size
         | 
| 753 | 
            -
                  puts "Done with #{@file} after #{count_executed_combinations} combinations"
         | 
| 754 | 
            -
                  return unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
         | 
| 755 | 
            -
             | 
| 756 | 
            -
                  puts 'The following changes were applied to the file:'
         | 
| 757 | 
            -
                  `diff #{experimental_filename(perfect_combination)} #{@file}`
         | 
| 758 | 
            -
                  puts "mv #{experimental_filename(perfect_combination)} #{@file}"
         | 
| 759 | 
            -
                  `mv #{experimental_filename(perfect_combination)} #{@file}`
         | 
| 760 | 
            -
                end
         | 
| 761 | 
            -
             | 
| 762 | 
            -
                def build_combinations
         | 
| 763 | 
            -
                  @round += 1
         | 
| 764 | 
            -
                  ExperimentCombinations.new(
         | 
| 765 | 
            -
                    round: @round,
         | 
| 766 | 
            -
                    occurrences_count: search_cases.size,
         | 
| 767 | 
            -
                    ok_experiments: @ok_experiments,
         | 
| 768 | 
            -
                    fail_experiments: @fail_experiments
         | 
| 769 | 
            -
                  ).generate_combinations
         | 
| 770 | 
            -
                end
         | 
| 771 | 
            -
             | 
| 772 | 
            -
                def run
         | 
| 773 | 
            -
                  while (combinations = build_combinations).any?
         | 
| 774 | 
            -
                    if combinations.size > 1000
         | 
| 775 | 
            -
                      puts "Ignoring #{@file} because it has #{combinations.size} possible combinations"
         | 
| 776 | 
            -
                      break
         | 
| 777 | 
            -
                    end
         | 
| 778 | 
            -
                    puts "#{@file} - Round #{@round} - Possible combinations: #{combinations.inspect}"
         | 
| 779 | 
            -
                    while combination = combinations.shift # rubocop:disable Lint/AssignmentInCondition
         | 
| 780 | 
            -
                      run_partial_replacement_with(combination)
         | 
| 781 | 
            -
                    end
         | 
| 782 | 
            -
                  end
         | 
| 783 | 
            -
                  done!
         | 
| 784 | 
            -
                end
         | 
| 785 | 
            -
             | 
| 786 | 
            -
                def run_partial_replacement_with(combination) # rubocop:disable Metrics/AbcSize
         | 
| 787 | 
            -
                  content = partial_replace(*combination)
         | 
| 788 | 
            -
                  experimental_file = experimental_filename(combination)
         | 
| 789 | 
            -
             | 
| 790 | 
            -
                  File.open(experimental_file, 'w+') { |f| f.puts content }
         | 
| 791 | 
            -
             | 
| 792 | 
            -
                  raise 'No changes were made to the file.' if FileUtils.compare_file(@file, experimental_file)
         | 
| 793 | 
            -
             | 
| 794 | 
            -
                  result = experiment.ok_if.call(experimental_file)
         | 
| 795 | 
            -
             | 
| 796 | 
            -
                  if result.success
         | 
| 797 | 
            -
                    ok_with(combination)
         | 
| 798 | 
            -
                    puts "✅ #{experimental_file} - Combination: #{combination} - Time: #{result.execution_time}s"
         | 
| 799 | 
            -
                  else
         | 
| 800 | 
            -
                    failed_with(combination)
         | 
| 801 | 
            -
                    puts "🔴 #{experimental_file} - Combination: #{combination}"
         | 
| 802 | 
            -
                  end
         | 
| 803 | 
            -
                end
         | 
| 804 | 
            -
              end
         | 
| 805 575 | 
             
            end
         | 
    
        data/lib/fast/cli.rb
    CHANGED
    
    
| @@ -0,0 +1,249 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'fast'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Fast
         | 
| 6 | 
            +
              class << self
         | 
| 7 | 
            +
                # Fast.experiment is a shortcut to define new experiments and allow them to
         | 
| 8 | 
            +
                # work together in experiment combinantions.
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # The following experiment look into `spec` folder and try to remove
         | 
| 11 | 
            +
                # `before` and `after` blocks on testing code. Sometimes they're not
         | 
| 12 | 
            +
                # effective and we can avoid the hard work of do it manually.
         | 
| 13 | 
            +
                #
         | 
| 14 | 
            +
                # If the spec does not fail, it keeps the change.
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                #  Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
         | 
| 17 | 
            +
                #    lookup 'spec'
         | 
| 18 | 
            +
                #    search "(block (send nil {before after}))"
         | 
| 19 | 
            +
                #    edit { |node| remove(node.loc.expression) }
         | 
| 20 | 
            +
                #    policy { |new_file| system("rspec --fail-fast #{new_file}") }
         | 
| 21 | 
            +
                #  end
         | 
| 22 | 
            +
                def experiment(name, &block)
         | 
| 23 | 
            +
                  @experiments ||= {}
         | 
| 24 | 
            +
                  @experiments[name] = Experiment.new(name, &block)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                attr_reader :experiments
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              # You can define experiments and build experimental files to improve some code in
         | 
| 31 | 
            +
              # an automated way. Let's create a hook to check if a `before` or `after` block
         | 
| 32 | 
            +
              # is useless in a specific spec:
         | 
| 33 | 
            +
              #
         | 
| 34 | 
            +
              # ```ruby
         | 
| 35 | 
            +
              # Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
         | 
| 36 | 
            +
              #   lookup 'some_spec.rb'
         | 
| 37 | 
            +
              #   search "(block (send nil {before after}))"
         | 
| 38 | 
            +
              #   edit {|node| remove(node.loc.expression) }
         | 
| 39 | 
            +
              #   policy {|new_file| system("bin/spring rspec --fail-fast #{new_file}") }
         | 
| 40 | 
            +
              # end
         | 
| 41 | 
            +
              # ```
         | 
| 42 | 
            +
              class Experiment
         | 
| 43 | 
            +
                attr_writer :files
         | 
| 44 | 
            +
                attr_reader :name, :replacement, :expression, :files_or_folders, :ok_if
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def initialize(name, &block)
         | 
| 47 | 
            +
                  @name = name
         | 
| 48 | 
            +
                  puts "\nStarting experiment: #{name}"
         | 
| 49 | 
            +
                  instance_exec(&block)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def run_with(file)
         | 
| 53 | 
            +
                  ExperimentFile.new(file, self).run
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def search(expression)
         | 
| 57 | 
            +
                  @expression = expression
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def edit(&block)
         | 
| 61 | 
            +
                  @replacement = block
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def lookup(files_or_folders)
         | 
| 65 | 
            +
                  @files_or_folders = files_or_folders
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def policy(&block)
         | 
| 69 | 
            +
                  @ok_if = block
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def files
         | 
| 73 | 
            +
                  @files ||= Fast.ruby_files_from(@files_or_folders)
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def run
         | 
| 77 | 
            +
                  files.map(&method(:run_with))
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
              # Suggest possible combinations of occurrences to replace.
         | 
| 82 | 
            +
              class ExperimentCombinations
         | 
| 83 | 
            +
                attr_reader :combinations
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def initialize(round:, occurrences_count:, ok_experiments:, fail_experiments:)
         | 
| 86 | 
            +
                  @round = round
         | 
| 87 | 
            +
                  @ok_experiments = ok_experiments
         | 
| 88 | 
            +
                  @fail_experiments = fail_experiments
         | 
| 89 | 
            +
                  @occurrences_count = occurrences_count
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def generate_combinations
         | 
| 93 | 
            +
                  case @round
         | 
| 94 | 
            +
                  when 1
         | 
| 95 | 
            +
                    individual_replacements
         | 
| 96 | 
            +
                  when 2
         | 
| 97 | 
            +
                    all_ok_replacements_combined
         | 
| 98 | 
            +
                  else
         | 
| 99 | 
            +
                    ok_replacements_pair_combinations
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                # Replace a single occurrence at each iteration and identify which
         | 
| 104 | 
            +
                # individual replacements work.
         | 
| 105 | 
            +
                def individual_replacements
         | 
| 106 | 
            +
                  (1..@occurrences_count).to_a
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                # After identifying all individual replacements that work, try combining all
         | 
| 110 | 
            +
                # of them.
         | 
| 111 | 
            +
                def all_ok_replacements_combined
         | 
| 112 | 
            +
                  [@ok_experiments.uniq.sort]
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                # Combining all successful individual replacements has failed. Lets divide
         | 
| 116 | 
            +
                # and conquer.
         | 
| 117 | 
            +
                def ok_replacements_pair_combinations
         | 
| 118 | 
            +
                  @ok_experiments
         | 
| 119 | 
            +
                    .combination(2)
         | 
| 120 | 
            +
                    .map { |e| e.flatten.uniq.sort }
         | 
| 121 | 
            +
                    .uniq - @fail_experiments - @ok_experiments
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              # Encapsulate the join of an Experiment with an specific file.
         | 
| 126 | 
            +
              # This is important to coordinate and regulate multiple experiments in the same file.
         | 
| 127 | 
            +
              # It can track successfull experiments and failures and suggest new combinations to keep replacing the file.
         | 
| 128 | 
            +
              class ExperimentFile
         | 
| 129 | 
            +
                attr_reader :ok_experiments, :fail_experiments, :experiment
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def initialize(file, experiment)
         | 
| 132 | 
            +
                  @file = file
         | 
| 133 | 
            +
                  @ast = Fast.ast_from_file(file) if file
         | 
| 134 | 
            +
                  @experiment = experiment
         | 
| 135 | 
            +
                  @ok_experiments = []
         | 
| 136 | 
            +
                  @fail_experiments = []
         | 
| 137 | 
            +
                  @round = 0
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def search
         | 
| 141 | 
            +
                  experiment.expression
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                def experimental_filename(combination)
         | 
| 145 | 
            +
                  parts = @file.split('/')
         | 
| 146 | 
            +
                  dir = parts[0..-2]
         | 
| 147 | 
            +
                  filename = "experiment_#{[*combination].join('_')}_#{parts[-1]}"
         | 
| 148 | 
            +
                  File.join(*dir, filename)
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def ok_with(combination)
         | 
| 152 | 
            +
                  @ok_experiments << combination
         | 
| 153 | 
            +
                  return unless combination.is_a?(Array)
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                  combination.each do |element|
         | 
| 156 | 
            +
                    @ok_experiments.delete(element)
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                def failed_with(combination)
         | 
| 161 | 
            +
                  @fail_experiments << combination
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def search_cases
         | 
| 165 | 
            +
                  Fast.search(@ast, experiment.expression) || []
         | 
| 166 | 
            +
                end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                # rubocop:disable Metrics/AbcSize
         | 
| 169 | 
            +
                # rubocop:disable Metrics/MethodLength
         | 
| 170 | 
            +
                def partial_replace(*indices)
         | 
| 171 | 
            +
                  replacement = experiment.replacement
         | 
| 172 | 
            +
                  new_content = Fast.replace_file @file, experiment.expression do |node, *captures|
         | 
| 173 | 
            +
                    if indices.nil? || indices.empty? || indices.include?(match_index)
         | 
| 174 | 
            +
                      if replacement.parameters.length == 1
         | 
| 175 | 
            +
                        instance_exec node, &replacement
         | 
| 176 | 
            +
                      else
         | 
| 177 | 
            +
                        instance_exec node, *captures, &replacement
         | 
| 178 | 
            +
                      end
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
                  return unless new_content
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  write_experiment_file(indices, new_content)
         | 
| 184 | 
            +
                  new_content
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
                # rubocop:enable Metrics/AbcSize
         | 
| 187 | 
            +
                # rubocop:enable Metrics/MethodLength
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                def write_experiment_file(index, new_content)
         | 
| 190 | 
            +
                  filename = experimental_filename(index)
         | 
| 191 | 
            +
                  File.open(filename, 'w+') { |f| f.puts new_content }
         | 
| 192 | 
            +
                  filename
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                def done!
         | 
| 196 | 
            +
                  count_executed_combinations = @fail_experiments.size + @ok_experiments.size
         | 
| 197 | 
            +
                  puts "Done with #{@file} after #{count_executed_combinations} combinations"
         | 
| 198 | 
            +
                  return unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                  puts 'The following changes were applied to the file:'
         | 
| 201 | 
            +
                  `diff #{experimental_filename(perfect_combination)} #{@file}`
         | 
| 202 | 
            +
                  puts "mv #{experimental_filename(perfect_combination)} #{@file}"
         | 
| 203 | 
            +
                  `mv #{experimental_filename(perfect_combination)} #{@file}`
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                def build_combinations
         | 
| 207 | 
            +
                  @round += 1
         | 
| 208 | 
            +
                  ExperimentCombinations.new(
         | 
| 209 | 
            +
                    round: @round,
         | 
| 210 | 
            +
                    occurrences_count: search_cases.size,
         | 
| 211 | 
            +
                    ok_experiments: @ok_experiments,
         | 
| 212 | 
            +
                    fail_experiments: @fail_experiments
         | 
| 213 | 
            +
                  ).generate_combinations
         | 
| 214 | 
            +
                end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                def run
         | 
| 217 | 
            +
                  while (combinations = build_combinations).any?
         | 
| 218 | 
            +
                    if combinations.size > 1000
         | 
| 219 | 
            +
                      puts "Ignoring #{@file} because it has #{combinations.size} possible combinations"
         | 
| 220 | 
            +
                      break
         | 
| 221 | 
            +
                    end
         | 
| 222 | 
            +
                    puts "#{@file} - Round #{@round} - Possible combinations: #{combinations.inspect}"
         | 
| 223 | 
            +
                    while combination = combinations.shift # rubocop:disable Lint/AssignmentInCondition
         | 
| 224 | 
            +
                      run_partial_replacement_with(combination)
         | 
| 225 | 
            +
                    end
         | 
| 226 | 
            +
                  end
         | 
| 227 | 
            +
                  done!
         | 
| 228 | 
            +
                end
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                def run_partial_replacement_with(combination) # rubocop:disable Metrics/AbcSize
         | 
| 231 | 
            +
                  content = partial_replace(*combination)
         | 
| 232 | 
            +
                  experimental_file = experimental_filename(combination)
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                  File.open(experimental_file, 'w+') { |f| f.puts content }
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                  raise 'No changes were made to the file.' if FileUtils.compare_file(@file, experimental_file)
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                  result = experiment.ok_if.call(experimental_file)
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                  if result.success
         | 
| 241 | 
            +
                    ok_with(combination)
         | 
| 242 | 
            +
                    puts "✅ #{experimental_file} - Combination: #{combination} - Time: #{result.execution_time}s"
         | 
| 243 | 
            +
                  else
         | 
| 244 | 
            +
                    failed_with(combination)
         | 
| 245 | 
            +
                    puts "🔴 #{experimental_file} - Combination: #{combination}"
         | 
| 246 | 
            +
                  end
         | 
| 247 | 
            +
                end
         | 
| 248 | 
            +
              end
         | 
| 249 | 
            +
            end
         | 
    
        data/lib/fast/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: ffast
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.8
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Jônatas Davi Paganini
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2019-03- | 
| 11 | 
            +
            date: 2019-03-27 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: astrolabe
         | 
| @@ -235,6 +235,7 @@ files: | |
| 235 235 | 
             
            - fast.gemspec
         | 
| 236 236 | 
             
            - lib/fast.rb
         | 
| 237 237 | 
             
            - lib/fast/cli.rb
         | 
| 238 | 
            +
            - lib/fast/experiment.rb
         | 
| 238 239 | 
             
            - lib/fast/version.rb
         | 
| 239 240 | 
             
            - mkdocs.yml
         | 
| 240 241 | 
             
            homepage: https://jonatas.github.io/fast/
         |