attractor 2.0.3 → 2.3.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -6
  3. data/Rakefile +13 -13
  4. data/app/assets/javascripts/index.pack.js +1 -1
  5. data/app/assets/stylesheets/main.css +1 -1
  6. data/exe/attractor +1 -1
  7. data/lib/attractor.rb +29 -13
  8. data/lib/attractor.rb~ +44 -0
  9. data/lib/attractor/cache.rb +82 -0
  10. data/lib/attractor/cache.rb~ +67 -0
  11. data/lib/attractor/calculators/base_calculator.rb +38 -14
  12. data/lib/attractor/calculators/base_calculator.rb~ +64 -0
  13. data/lib/attractor/cli.rb +48 -44
  14. data/lib/attractor/cli.rb~ +84 -0
  15. data/lib/attractor/detectors/base_detector.rb~ +4 -0
  16. data/lib/attractor/duration_parser.rb +7 -5
  17. data/lib/attractor/duration_parser.rb~ +27 -0
  18. data/lib/attractor/gem_names.rb +3 -1
  19. data/lib/attractor/gem_names.rb~ +23 -0
  20. data/lib/attractor/registry_entry.rb +0 -1
  21. data/lib/attractor/registry_entry.rb~ +2 -0
  22. data/lib/attractor/reporters/base_reporter.rb +22 -14
  23. data/lib/attractor/reporters/base_reporter.rb~ +54 -0
  24. data/lib/attractor/reporters/console_reporter.rb +4 -4
  25. data/lib/attractor/reporters/console_reporter.rb~ +21 -0
  26. data/lib/attractor/reporters/html_reporter.rb +23 -25
  27. data/lib/attractor/reporters/html_reporter.rb~ +48 -0
  28. data/lib/attractor/reporters/sinatra_reporter.rb +18 -24
  29. data/lib/attractor/suggester.rb +3 -1
  30. data/lib/attractor/tmp/churn/9f86bc9b7ec6bf59cbf7a111ce8b1159ae128347.json +1 -0
  31. data/lib/attractor/value.rb +8 -4
  32. data/lib/attractor/value.rb~ +30 -0
  33. data/lib/attractor/version.rb +1 -1
  34. data/lib/attractor/watcher.rb +7 -3
  35. data/lib/attractor/watcher.rb~ +24 -0
  36. metadata +29 -31
  37. data/.babelrc +0 -4
  38. data/.eslintrc.json +0 -25
  39. data/.gitignore +0 -20
  40. data/.rubocop.yml +0 -2
  41. data/CHANGELOG.md +0 -99
  42. data/Gemfile +0 -4
  43. data/Guardfile +0 -27
  44. data/attractor.gemspec +0 -65
  45. data/bin/console +0 -14
  46. data/bin/setup +0 -8
  47. data/bin/standardize +0 -4
  48. data/bin/test +0 -6
  49. data/dist/calculator.bundle.js +0 -1
  50. data/lib/attractor/reporters/.keep +0 -0
  51. data/package-lock.json +0 -6906
  52. data/package.json +0 -53
  53. data/webpack.config.js +0 -23
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "churn/calculator"
4
+
5
+ require "attractor/value"
6
+
7
+ module Attractor
8
+ # calculates churn and complexity
9
+ class BaseCalculator
10
+ attr_reader :type
11
+
12
+ def initialize(file_prefix: "", ignores: "", file_extension: "rb", minimum_churn_count: 3, start_ago: "5y")
13
+ @file_prefix = file_prefix
14
+ @file_extension = file_extension
15
+ @minimum_churn_count = minimum_churn_count
16
+ @start_date = Date.today - Attractor::DurationParser.new(start_ago).duration
17
+ @ignores = ignores
18
+ end
19
+
20
+ def calculate
21
+ churn = ::Churn::ChurnCalculator.new(
22
+ file_extension: @file_extension,
23
+ file_prefix: @file_prefix,
24
+ minimum_churn_count: @minimum_churn_count,
25
+ start_date: @start_date,
26
+ ignores: @ignores
27
+ ).report(false)
28
+
29
+ churn[:churn][:changes].map do |change|
30
+ history = git_history_for_file(file_path: change[:file_path])
31
+ commit = history&.first&.first
32
+
33
+ cached_value = Cache.read(file_path: change[:file_path])
34
+
35
+ if !cached_value.nil? && !cached_value.current_commit.nil? && cached_value.current_commit == commit
36
+ value = cached_value
37
+ else
38
+ complexity, details = yield(change)
39
+
40
+ value = Value.new(file_path: change[:file_path],
41
+ churn: change[:times_changed],
42
+ complexity: complexity,
43
+ details: details,
44
+ history: history)
45
+ Cache.write(file_path: change[:file_path], value: value)
46
+ end
47
+
48
+ value
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def git_history_for_file(file_path:, limit: 10)
55
+ history = `git log --oneline -n #{limit} -- #{file_path}`
56
+ history.split("\n")
57
+ .map do |log_entry|
58
+ log_entry.partition(/\A(\S+)\s/)
59
+ .map(&:strip)
60
+ .reject(&:empty?)
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/attractor/cli.rb CHANGED
@@ -1,95 +1,99 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
3
+ require "thor"
4
4
 
5
- require 'attractor'
5
+ require "attractor"
6
6
 
7
7
  module Attractor
8
8
  # contains methods implementing the CLI
9
9
  class CLI < Thor
10
10
  shared_options = [[:file_prefix, aliases: :p],
11
- [:watch, aliases: :w, type: :boolean],
12
- [:minimum_churn, aliases: :c, type: :numeric, default: 3],
13
- [:start_ago, aliases: :s, type: :string, default: '5y'],
14
- [:type, aliases: :t]]
11
+ [:ignore, aliases: :i, default: ""],
12
+ [:watch, aliases: :w, type: :boolean],
13
+ [:minimum_churn, aliases: :c, type: :numeric, default: 3],
14
+ [:start_ago, aliases: :s, type: :string, default: "5y"],
15
+ [:type, aliases: :t]]
15
16
 
16
- advanced_options = [[:format, aliases: :f, default: 'html'],
17
- [:no_open_browser, type: :boolean],
18
- [:ci, type: :boolean]]
17
+ advanced_options = [[:format, aliases: :f, default: "html"],
18
+ [:no_open_browser, type: :boolean],
19
+ [:ci, type: :boolean]]
19
20
 
20
21
  desc "version", "Prints Attractor's version information"
21
- map %w(-v --version) => :version
22
+ map %w[-v --version] => :version
22
23
  def version
23
24
  puts "Attractor version #{Attractor::VERSION}"
24
25
  rescue RuntimeError => e
25
26
  puts "Runtime error: #{e.message}"
26
27
  end
27
28
 
28
- desc 'calc', 'Calculates churn and complexity for all ruby files in current directory'
29
+ desc "clean", "Clears attractor's cache"
30
+ def clean
31
+ puts "Clearing attractor cache"
32
+ Attractor.clear
33
+ end
34
+
35
+ desc "init", "Initializes attractor's cache"
36
+ shared_options.each do |shared_option|
37
+ option(*shared_option)
38
+ end
39
+ def init
40
+ puts "Warming attractor cache"
41
+ Attractor.init(calculators(options))
42
+ end
43
+
44
+ desc "calc", "Calculates churn and complexity for all ruby files in current directory"
29
45
  shared_options.each do |shared_option|
30
46
  option(*shared_option)
31
47
  end
32
48
  def calc
33
49
  file_prefix = options[:file_prefix]
34
- if options[:watch]
35
- puts 'Listening for file changes...'
36
- Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators(options)).watch
37
- else
38
- Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators(options)).report
39
- end
50
+
51
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options))
40
52
  rescue RuntimeError => e
41
53
  puts "Runtime error: #{e.message}"
42
54
  end
43
55
 
44
- desc 'report', 'Generates an HTML report'
56
+ desc "report", "Generates an HTML report"
45
57
  (shared_options + advanced_options).each do |option|
46
58
  option(*option)
47
59
  end
48
60
  def report
49
61
  file_prefix = options[:file_prefix]
50
62
  open_browser = !(options[:no_open_browser] || options[:ci])
51
- if options[:watch]
52
- puts 'Listening for file changes...'
53
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).watch
54
- else
55
- case options[:format]
56
- when 'html'
57
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
58
- else
59
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
60
- end
61
- end
63
+
64
+ report! Attractor::HtmlReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
62
65
  rescue RuntimeError => e
63
66
  puts "Runtime error: #{e.message}"
64
67
  end
65
68
 
66
- desc 'serve', 'Serves the report on localhost'
69
+ desc "serve", "Serves the report on localhost"
67
70
  (shared_options + advanced_options).each do |option|
68
71
  option(*option)
69
72
  end
70
73
  def serve
71
74
  file_prefix = options[:file_prefix]
72
75
  open_browser = !(options[:no_open_browser] || options[:ci])
73
- if options[:watch]
74
- puts 'Listening for file changes...'
75
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).watch
76
- else
77
- case options[:format]
78
- when 'html'
79
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
80
- else
81
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
82
- end
83
- end
76
+
77
+ report! Attractor::SinatraReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
84
78
  end
85
79
 
86
80
  private
87
81
 
88
82
  def calculators(options)
89
83
  Attractor.calculators_for_type(options[:type],
90
- file_prefix: options[:file_prefix],
91
- minimum_churn_count: options[:minimum_churn],
92
- start_ago: options[:start_ago])
84
+ file_prefix: options[:file_prefix],
85
+ minimum_churn_count: options[:minimum_churn],
86
+ ignores: options[:ignore],
87
+ start_ago: options[:start_ago])
88
+ end
89
+
90
+ def report!(reporter)
91
+ if options[:watch]
92
+ puts "Listening for file changes..."
93
+ reporter.watch
94
+ else
95
+ reporter.report
96
+ end
93
97
  end
94
98
  end
95
99
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ require "attractor"
6
+
7
+ module Attractor
8
+ # contains methods implementing the CLI
9
+ class CLI < Thor
10
+ shared_options = [[:file_prefix, aliases: :p],
11
+ [:ignore, aliases: :i, default: ""],
12
+ [:watch, aliases: :w, type: :boolean],
13
+ [:minimum_churn, aliases: :c, type: :numeric, default: 3],
14
+ [:start_ago, aliases: :s, type: :string, default: "5y"],
15
+ [:type, aliases: :t]]
16
+
17
+ advanced_options = [[:format, aliases: :f, default: "html"],
18
+ [:no_open_browser, type: :boolean],
19
+ [:ci, type: :boolean]]
20
+
21
+ desc "version", "Prints Attractor's version information"
22
+ map %w[-v --version] => :version
23
+ def version
24
+ puts "Attractor version #{Attractor::VERSION}"
25
+ rescue RuntimeError => e
26
+ puts "Runtime error: #{e.message}"
27
+ end
28
+
29
+ desc "calc", "Calculates churn and complexity for all ruby files in current directory"
30
+ shared_options.each do |shared_option|
31
+ option(*shared_option)
32
+ end
33
+ def calc
34
+ file_prefix = options[:file_prefix]
35
+
36
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options))
37
+ rescue RuntimeError => e
38
+ puts "Runtime error: #{e.message}"
39
+ end
40
+
41
+ desc "report", "Generates an HTML report"
42
+ (shared_options + advanced_options).each do |option|
43
+ option(*option)
44
+ end
45
+ def report
46
+ file_prefix = options[:file_prefix]
47
+ open_browser = !(options[:no_open_browser] || options[:ci])
48
+
49
+ report! Attractor::HtmlReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
50
+ rescue RuntimeError => e
51
+ puts "Runtime error: #{e.message}"
52
+ end
53
+
54
+ desc "serve", "Serves the report on localhost"
55
+ (shared_options + advanced_options).each do |option|
56
+ option(*option)
57
+ end
58
+ def serve
59
+ file_prefix = options[:file_prefix]
60
+ open_browser = !(options[:no_open_browser] || options[:ci])
61
+
62
+ report! Attractor::SinatraReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
63
+ end
64
+
65
+ private
66
+
67
+ def calculators(options)
68
+ Attractor.calculators_for_type(options[:type],
69
+ file_prefix: options[:file_prefix],
70
+ minimum_churn_count: options[:minimum_churn],
71
+ ignores: options[:ignore],
72
+ start_ago: options[:start_ago])
73
+ end
74
+
75
+ def report!(reporter)
76
+ if options[:watch]
77
+ puts "Listening for file changes..."
78
+ reporter.watch
79
+ else
80
+ reporter.report
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,4 @@
1
+ module Attractor
2
+ class BaseDetector
3
+ end
4
+ end
@@ -4,17 +4,19 @@ module Attractor
4
4
  # converts a duration string into an amount of days
5
5
  class DurationParser
6
6
  TOKENS = {
7
- 'd' => 1,
8
- 'w' => 7,
9
- 'm' => 30,
10
- 'y' => 365
7
+ "d" => 1,
8
+ "w" => 7,
9
+ "m" => 30,
10
+ "y" => 365
11
11
  }.freeze
12
12
 
13
13
  attr_reader :duration
14
14
 
15
15
  def initialize(input)
16
16
  @input = input
17
- @duration = 0
17
+ @duration = @input.is_a?(Numeric) ? @input : 0
18
+ return if @duration > 0
19
+
18
20
  parse
19
21
  end
20
22
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attractor
4
+ # converts a duration string into an amount of days
5
+ class DurationParser
6
+ TOKENS = {
7
+ "d" => 1,
8
+ "w" => 7,
9
+ "m" => 30,
10
+ "y" => 365
11
+ }.freeze
12
+
13
+ attr_reader :duration
14
+
15
+ def initialize(input)
16
+ @input = input
17
+ @duration = 0
18
+ parse
19
+ end
20
+
21
+ def parse
22
+ @input.scan(/(\d+)(\w)/).each do |amount, measure|
23
+ @duration += amount.to_i * TOKENS[measure]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -2,7 +2,7 @@ module Attractor
2
2
  # from https://github.com/prontolabs/pronto/blob/master/lib/pronto/gem_names.rb
3
3
  class GemNames
4
4
  def to_a
5
- gems.map { |gem| gem.name.sub(/^attractor-/, '') }.uniq.sort
5
+ gems.map { |gem| gem.name.sub(/^attractor-/, "") }.uniq.sort
6
6
  end
7
7
 
8
8
  private
@@ -10,6 +10,8 @@ module Attractor
10
10
  def gems
11
11
  Gem::Specification.find_all.select do |gem|
12
12
  gem.name =~ /^attractor-/
13
+ end.reject do |gem|
14
+ gem.name =~ /attractor-rails/
13
15
  end
14
16
  end
15
17
  end
@@ -0,0 +1,23 @@
1
+ module Attractor
2
+
3
+ # from https://github.com/prontolabs/pronto/blob/master/lib/pronto/gem_names.rb
4
+ class GemNames
5
+ def to_a
6
+ gems.map { |gem| gem.name.sub(/^pronto-/, '') }.uniq.sort
7
+ end
8
+
9
+ private
10
+
11
+ def gems
12
+ Gem::Specification.find_all.select do |gem|
13
+ if gem.name =~ /^pronto-/
14
+ true
15
+ elsif gem.name != 'pronto'
16
+ runner_path = File.join(gem.full_gem_path,
17
+ "lib/pronto/#{gem.name}.rb")
18
+ File.exist?(runner_path)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -9,4 +9,3 @@ module Attractor
9
9
  end
10
10
  end
11
11
  end
12
-
@@ -0,0 +1,2 @@
1
+ RegistryEntry = Struct.new :type, :detector, :calculator
2
+
@@ -1,34 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'descriptive_statistics/safe'
4
- require 'fileutils'
5
- require 'forwardable'
6
- require 'launchy'
7
- require 'tilt'
3
+ require "descriptive_statistics/safe"
4
+ require "fileutils"
5
+ require "forwardable"
6
+ require "launchy"
7
+ require "tilt"
8
8
 
9
9
  module Attractor
10
10
  # base reporter
11
11
  class BaseReporter
12
12
  extend Forwardable
13
- attr_accessor :values, :file_prefix
13
+ attr_accessor :file_prefix
14
14
  attr_reader :types
15
+ attr_writer :values
15
16
  def_delegator :@watcher, :watch
16
17
 
17
- def initialize(file_prefix: '', calculators:, open_browser: true)
18
- @file_prefix = file_prefix
18
+ def initialize(calculators:, file_prefix: "", ignores: "", open_browser: true)
19
+ @file_prefix = file_prefix || ""
19
20
  @calculators = calculators
20
21
  @open_browser = open_browser
21
- @values = @calculators.first.last.calculate
22
22
  @suggester = Suggester.new(values)
23
23
 
24
- @watcher = Watcher.new(file_prefix, lambda do
24
+ @watcher = Watcher.new(@file_prefix, ignores, lambda do
25
25
  report
26
26
  end)
27
- rescue NoMethodError => e
28
- raise 'There was a problem gathering churn changes'
27
+ rescue NoMethodError => _e
28
+ raise "There was a problem gathering churn changes"
29
29
  end
30
30
 
31
- def suggestions(quantile)
31
+ def suggestions(quantile:, type: "rb")
32
+ @suggester.values = values(type: type)
32
33
  @suggestions = @suggester.suggest(quantile)
33
34
  @suggestions
34
35
  end
@@ -39,7 +40,14 @@ module Attractor
39
40
  end
40
41
 
41
42
  def render
42
- 'Attractor'
43
+ "Attractor"
44
+ end
45
+
46
+ def values(type: "rb")
47
+ @values = @calculators[type].calculate
48
+ @values
49
+ rescue NoMethodError => _e
50
+ puts "No calculator for type #{type}"
43
51
  end
44
52
  end
45
53
  end