ffast 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c7eee39cf27764a49009037c603ebd8f9afe0b8f0d78ac651c2bdee1d65f329
4
- data.tar.gz: '09eefe65daeeb37ed5d49120cbee73018303156df11f25daa5258ce310a1be89'
3
+ metadata.gz: a8e1cdbb944bd8939745c07350aaa66e4ff3e9e3313802ca23aaa7fbd33b742d
4
+ data.tar.gz: fcd59e90fedc893492aa791e129d18a8b2e2e72e72b8ca61be6be8fd3e4ad6b7
5
5
  SHA512:
6
- metadata.gz: 163730827f44a4d9a5491b47f96e6d3f10d883921710b5fa2e6f9573a586e71ff84460710288b3388ef209c90744f01d7dfc03ca5f5daff5a62c2179e3da43c5
7
- data.tar.gz: 0ecc89dd396c151c07fcf6bc9de52c3dc183909bb29498715f2e703124db75e03847894a3d7394e79b9beabee750fa13db23d74bcc95b68a721553d6f5b8c46b
6
+ metadata.gz: f33ebcf4e4275e783d75a761b78fc54a70f789f89ea735011a5a5628c7d2f8630271e29436aa1d3a15418f4186fc42764fad3ca5d72ef69bf2ccc860424299a3
7
+ data.tar.gz: ab1c3275a3415aba183ac6e558284d4f30bb5e462454758808df430059f077eb141f18a8ec725ad7738e1e9e3fd350688d674f6691a4d6165e17f039db34c653
data/README.md CHANGED
@@ -319,18 +319,29 @@ You can use `search_file` and pass the path for search for expressions inside
319
319
  files.
320
320
 
321
321
  ```ruby
322
- Fast.search_file('file.rb', expression)
322
+ Fast.search_file(expression, 'file.rb')
323
323
  ```
324
324
 
325
325
  It's simple combination of `Fast.ast_from_file` with `Fast.search`.
326
326
 
327
+
328
+ ## Fast.capture_file
329
+
330
+ You can also use `Fast.capture_file` that is similar but return only the
331
+ captures:
332
+
333
+ ```ruby
334
+ Fast.capture_file('(class (const nil $_))', 'lib/fast.rb')
335
+ # => [:Rewriter, :ExpressionParser, :Find, :FindString, ...]
336
+ ```
337
+
327
338
  ## Fast.ruby_files_from(arguments)
328
339
 
329
340
  You'll be probably looking for multiple ruby files, then this method fetches
330
341
  all internal `.rb` files
331
342
 
332
343
  ```ruby
333
- Fast.ruby_files_from(['lib']) # => ["lib/fast.rb"]
344
+ Fast.ruby_files_from('lib') # => ["lib/fast.rb"]
334
345
  ```
335
346
 
336
347
  ## `fast` in the command line
@@ -423,7 +434,7 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
423
434
 
424
435
  On the console we have a few functions like `s` and `code` to make it easy ;)
425
436
 
426
- $ bin/console
437
+ $ bin/console
427
438
 
428
439
  ```ruby
429
440
  code("a = 1") # => s(:lvasgn, s(:int, 1))
@@ -13,9 +13,7 @@ experiment_files.each(&method(:require))
13
13
  experiments = []
14
14
 
15
15
  if arguments.any?
16
- while arguments.any? && !(File.exist?(arguments.first) || Dir.exist?(arguments.first))
17
- experiments << arguments.shift
18
- end
16
+ experiments << arguments.shift while arguments.any? && !(File.exist?(arguments.first) || Dir.exist?(arguments.first))
19
17
  experiments.map! { |name| Fast.experiments[name] }
20
18
  else
21
19
  experiments = Fast.experiments.values
@@ -0,0 +1,27 @@
1
+
2
+ You can create a custom command in pry to reuse fast in any session.
3
+
4
+ Start simply dropping it on your `.pryrc`:
5
+
6
+ ```ruby
7
+ Pry::Commands.block_command "fast", "Fast search" do |expression, file|
8
+ require "fast"
9
+ files = Fast.ruby_files_from(file || '.')
10
+ files.each do |f|
11
+ results = Fast.search_file(expression, f)
12
+ next if results.nil? || results.empty?
13
+ output.puts Fast.highlight("# #{f}")
14
+
15
+ results.each do |result|
16
+ output.puts Fast.highlight(result)
17
+ end
18
+ end
19
+ end
20
+ ```
21
+
22
+ And use it in the console:
23
+
24
+ `pry
25
+ fast '(def match?)' lib/fast.rb`
26
+ ```
27
+
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast'
4
+ require 'open3'
5
+ require 'ostruct'
6
+ require 'fileutils'
7
+
8
+ # Usage instructions:
9
+ # 1. Add the following to your project's Gemfile: gem 'ffast' (yes, two "f"s)
10
+ # 2. Copy this file to your Rails project root directory
11
+ # 3. Run: bundle exec ruby build_stubbed_and_let_it_be_experiment.rb
12
+
13
+ # List of spec files you want to experiment with. One per line.
14
+ FILE_NAMES = %w[
15
+ # spec/model/foo_spec.rb
16
+ # spec/model/bar_spec.rb
17
+ ]
18
+
19
+ def execute_rspec(file_name)
20
+ rspec_command = "bin/spring rspec --fail-fast --format progress #{file_name}"
21
+ stdout_str, stderr_str, status = Open3.capture3(rspec_command)
22
+ execution_time = /Finished in (.*?) seconds/.match(stdout_str)[1]
23
+ print stderr_str.gsub(/Running via Spring preloader.*?$/, '').chomp unless status.success?
24
+ OpenStruct.new(success: status.success?, execution_time: execution_time)
25
+ end
26
+
27
+ def delete_temp_files(original_file_name)
28
+ file_path = File.dirname(original_file_name)
29
+ file_name = File.basename(original_file_name)
30
+ Dir.glob("#{file_path}/experiment*#{file_name}").each { |file| File.delete(file)}
31
+ end
32
+
33
+ FILE_NAMES.each do |original_file_name|
34
+ Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do
35
+ lookup original_file_name
36
+ search '(block (send nil let (sym _)) (args) $(send nil create))'
37
+ edit { |_, (create)| replace(create.loc.selector, 'build_stubbed') }
38
+ policy { |experiment_file_name| execute_rspec(experiment_file_name) }
39
+ end.run
40
+
41
+ Fast.experiment('RSpec/LetItBe') do
42
+ lookup original_file_name
43
+ search '(block $(send nil let! (sym _)) (args) (send nil create))'
44
+ edit { |_, (let)| replace(let.loc.selector, 'let_it_be') }
45
+ policy { |experiment_file_name| execute_rspec(experiment_file_name) }
46
+ end.run
47
+
48
+ delete_temp_files(original_file_name)
49
+ end
50
+
51
+
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+
3
5
  # suppress output to avoid parser gem warnings'
4
6
  def suppress_output
5
7
  original_stdout = $stdout.clone
@@ -21,7 +23,7 @@ end
21
23
  module Fast
22
24
  LITERAL = {
23
25
  '...' => ->(node) { node&.children&.any? },
24
- '_' => ->(node) { !node.nil? },
26
+ '_' => ->(node) { !node.nil? },
25
27
  'nil' => nil
26
28
  }.freeze
27
29
 
@@ -55,7 +57,7 @@ module Fast
55
57
  \\\d # find using captured expression
56
58
  |
57
59
  %\d # find using binded argument
58
- /x
60
+ /x.freeze
59
61
 
60
62
  class << self
61
63
  def match?(ast, search, *args)
@@ -85,6 +87,11 @@ module Fast
85
87
  search node, pattern
86
88
  end
87
89
 
90
+ def capture_file(pattern, file)
91
+ node = ast_from_file(file)
92
+ capture node, pattern
93
+ end
94
+
88
95
  def search(node, pattern)
89
96
  if (match = Fast.match?(node, pattern))
90
97
  yield node, match if block_given?
@@ -93,7 +100,7 @@ module Fast
93
100
  node.children
94
101
  .grep(Parser::AST::Node)
95
102
  .flat_map { |e| search(e, pattern) }
96
- .compact.uniq.flatten
103
+ .compact.flatten
97
104
  end
98
105
  end
99
106
 
@@ -577,6 +584,7 @@ module Fast
577
584
 
578
585
  def initialize(name, &block)
579
586
  @name = name
587
+ puts "\nStarting experiment: #{name}"
580
588
  instance_exec(&block)
581
589
  end
582
590
 
@@ -609,17 +617,63 @@ module Fast
609
617
  end
610
618
  end
611
619
 
620
+ # Suggest possible combinations of occurrences to replace.
621
+ class ExperimentCombinations
622
+ attr_reader :combinations
623
+
624
+ def initialize(round:, occurrences_count:, ok_experiments:, fail_experiments:)
625
+ @round = round
626
+ @ok_experiments = ok_experiments
627
+ @fail_experiments = fail_experiments
628
+ @occurrences_count = occurrences_count
629
+ end
630
+
631
+ def generate_combinations
632
+ case @round
633
+ when 1
634
+ individual_replacements
635
+ when 2
636
+ all_ok_replacements_combined
637
+ else
638
+ ok_replacements_pair_combinations
639
+ end
640
+ end
641
+
642
+ # Replace a single occurrence at each iteration and identify which
643
+ # individual replacements work.
644
+ def individual_replacements
645
+ (1..@occurrences_count).to_a
646
+ end
647
+
648
+ # After identifying all individual replacements that work, try combining all
649
+ # of them.
650
+ def all_ok_replacements_combined
651
+ [@ok_experiments.uniq.sort]
652
+ end
653
+
654
+ # Combining all successful individual replacements has failed. Lets divide
655
+ # and conquer.
656
+ def ok_replacements_pair_combinations
657
+ @ok_experiments
658
+ .combination(2)
659
+ .map { |e| e.flatten.uniq.sort }
660
+ .uniq - @fail_experiments - @ok_experiments
661
+ end
662
+ end
663
+
612
664
  # Encapsulate the join of an Experiment with an specific file.
613
665
  # This is important to coordinate and regulate multiple experiments in the same file.
614
666
  # It can track successfull experiments and failures and suggest new combinations to keep replacing the file.
615
667
  class ExperimentFile
616
668
  attr_reader :ok_experiments, :fail_experiments, :experiment
669
+
617
670
  def initialize(file, experiment)
618
671
  @file = file
619
672
  @ast = Fast.ast_from_file(file) if file
620
673
  @experiment = experiment
621
674
  @ok_experiments = []
622
675
  @fail_experiments = []
676
+ @round = 0
623
677
  end
624
678
 
625
679
  def search
@@ -677,33 +731,34 @@ module Fast
677
731
  filename
678
732
  end
679
733
 
680
- def suggest_combinations
681
- if @ok_experiments.empty? && @fail_experiments.empty?
682
- (1..search_cases.size).to_a
683
- else
684
- @ok_experiments
685
- .combination(2)
686
- .map { |e| e.flatten.uniq.sort }
687
- .uniq - @fail_experiments - @ok_experiments
688
- end
689
- end
690
-
691
734
  def done!
692
735
  count_executed_combinations = @fail_experiments.size + @ok_experiments.size
693
- puts "Done with #{@file} after #{count_executed_combinations}"
736
+ puts "Done with #{@file} after #{count_executed_combinations} combinations"
694
737
  return unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
695
738
 
739
+ puts 'The following changes were applied to the file:'
740
+ `diff #{experimental_filename(perfect_combination)} #{@file}`
696
741
  puts "mv #{experimental_filename(perfect_combination)} #{@file}"
697
742
  `mv #{experimental_filename(perfect_combination)} #{@file}`
698
743
  end
699
744
 
745
+ def build_combinations
746
+ @round += 1
747
+ ExperimentCombinations.new(
748
+ round: @round,
749
+ occurrences_count: search_cases.size,
750
+ ok_experiments: @ok_experiments,
751
+ fail_experiments: @fail_experiments
752
+ ).generate_combinations
753
+ end
754
+
700
755
  def run
701
- while (combinations = suggest_combinations).any?
702
- if combinations.size > 50
703
- puts "Ignoring #{@file} because it have #{combinations.size} possible combinations"
756
+ while (combinations = build_combinations).any?
757
+ if combinations.size > 1000
758
+ puts "Ignoring #{@file} because it has #{combinations.size} possible combinations"
704
759
  break
705
760
  end
706
- puts "#{@file} - Possible combinations: #{combinations.inspect}"
761
+ puts "#{@file} - Round #{@round} - Possible combinations: #{combinations.inspect}"
707
762
  while combination = combinations.shift # rubocop:disable Lint/AssignmentInCondition
708
763
  run_partial_replacement_with(combination)
709
764
  end
@@ -711,28 +766,23 @@ module Fast
711
766
  done!
712
767
  end
713
768
 
714
- # rubocop:disable Metrics/MethodLength
715
- # rubocop:disable Metrics/AbcSize
716
- def run_partial_replacement_with(combination)
717
- puts "#{@file} applying partial replacement with: #{combination}"
769
+ def run_partial_replacement_with(combination) # rubocop:disable Metrics/AbcSize
718
770
  content = partial_replace(*combination)
719
771
  experimental_file = experimental_filename(combination)
720
- puts `diff #{experimental_file} #{@file}`
721
- if experimental_file == IO.read(@file)
722
- raise 'Returned the same file thinking:'
723
- end
724
772
 
725
773
  File.open(experimental_file, 'w+') { |f| f.puts content }
726
774
 
727
- if experiment.ok_if.call(experimental_file)
775
+ raise 'No changes were made to the file.' if FileUtils.compare_file(@file, experimental_file)
776
+
777
+ result = experiment.ok_if.call(experimental_file)
778
+
779
+ if result.success
728
780
  ok_with(combination)
729
- puts "✅ #{combination} #{experimental_file}"
781
+ puts "✅ #{experimental_file} - Combination: #{combination} - Time: #{result.execution_time}s"
730
782
  else
731
783
  failed_with(combination)
732
- puts "🔴 #{combination} #{experimental_file}"
784
+ puts "🔴 #{experimental_file} - Combination: #{combination}"
733
785
  end
734
786
  end
735
- # rubocop:enable Metrics/MethodLength
736
- # rubocop:enable Metrics/AbcSize
737
787
  end
738
788
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fast
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.5'
5
5
  end
data/mkdocs.yml CHANGED
@@ -17,5 +17,6 @@ nav:
17
17
  - Introduction: index.md
18
18
  - Syntax: syntax.md
19
19
  - Command Line: command_line.md
20
+ - Pry Integration: pry-integration.md
20
21
  - Experiments: experiments.md
21
22
  - Code Similarity: similarity_tutorial.md
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
4
+ version: 0.0.5
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: 2018-10-22 00:00:00.000000000 Z
11
+ date: 2019-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: coderay
@@ -205,8 +205,10 @@ files:
205
205
  - docs/command_line.md
206
206
  - docs/experiments.md
207
207
  - docs/index.md
208
+ - docs/pry-integration.md
208
209
  - docs/similarity_tutorial.md
209
210
  - docs/syntax.md
211
+ - examples/build_stubbed_and_let_it_be_experiment.rb
210
212
  - examples/experimental_replacement.rb
211
213
  - examples/find_usage.rb
212
214
  - examples/let_it_be_experiment.rb