attractor 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1b66250e293edf3d4298ee78c92a928b92128d0f8525a012ba4c99b8e16a77e
4
- data.tar.gz: 354068cdc6c91978e80e3860ac65255811daf364d64c15dcd7221db2bac79f22
3
+ metadata.gz: 1eef3d6e8104428fdc8240ae5161654990b53d5351962bc474f11e450db9cbc2
4
+ data.tar.gz: 6ec7109f7c6aa6e4b5b594863e5bfeff08652f2fdf6c8c2a86a208357c084713
5
5
  SHA512:
6
- metadata.gz: 456416f564b3e7e5efa7874f901fea6a0c26c23e59bf157dd5c78cb58014f4bcee58f0172cebdc83061d94195d94631e1818eba905835799e1664527fc26caa9
7
- data.tar.gz: 472072d37700e794de8ee98d48f2252186598f709094c9046acc97deb91ef34dfbc089a4c7b70f0965ce8fc4621877c63d41a167f4d2968f2b734b9f5e4e18de
6
+ metadata.gz: 244a2d549ba546424c7a4c25aaa59c17b4bd1a8fed73cbdd09851c4296631c4b42d907d4b31a76a8a4f6c584143d8f086764299e00e690bce1f0f19cc69d8bcb
7
+ data.tar.gz: d508623821df269d803f3fdb0440fbd76f14a5d24cbd169c23cfabf01e1423171b201b1c3ee2c92a986ada20700c790c67ff75d3928bfbf9e4e1d2419e60be65
data/Rakefile CHANGED
@@ -25,7 +25,7 @@ task :assets do
25
25
  sass = File.read(File.expand_path("./src/stylesheets/main.scss"))
26
26
  css = SassC::Engine.new(sass, style: :compressed).render
27
27
  prefixed = AutoprefixerRails.process(css)
28
- File.open(File.expand_path("./app/assets/stylesheets/main.css"), "w") { |file| file.write(prefixed) }
28
+ File.write(File.expand_path("./app/assets/stylesheets/main.css"), prefixed)
29
29
 
30
30
  npm_output = `npm run build`
31
31
  puts npm_output
@@ -9,12 +9,13 @@ module Attractor
9
9
  class BaseCalculator
10
10
  attr_reader :type
11
11
 
12
- def initialize(file_prefix: "", ignores: "", file_extension: "rb", minimum_churn_count: 3, start_ago: "5y")
12
+ def initialize(file_prefix: "", ignores: "", file_extension: "rb", minimum_churn_count: 3, start_ago: "5y", verbose: false)
13
13
  @file_prefix = file_prefix
14
14
  @file_extension = file_extension
15
15
  @minimum_churn_count = minimum_churn_count
16
16
  @start_date = Date.today - Attractor::DurationParser.new(start_ago).duration
17
17
  @ignores = ignores
18
+ @verbose = verbose
18
19
  end
19
20
 
20
21
  def calculate
@@ -26,7 +27,7 @@ module Attractor
26
27
  ignores: @ignores
27
28
  ).report(false)
28
29
 
29
- puts "Calculating churn and complexity values for #{churn[:churn][:changes].size} #{type} files"
30
+ puts "Calculating churn and complexity values for #{churn[:churn][:changes].size} #{type} files" if @verbose
30
31
 
31
32
  values = churn[:churn][:changes].map do |change|
32
33
  history = git_history_for_file(file_path: change[:file_path])
@@ -47,13 +48,13 @@ module Attractor
47
48
  Cache.write(file_path: change[:file_path], value: value)
48
49
  end
49
50
 
50
- print "."
51
+ print "." if @verbose
51
52
  value
52
53
  end
53
54
 
54
55
  Cache.persist!
55
56
 
56
- print "\n\n"
57
+ print "\n\n" if @verbose
57
58
 
58
59
  values
59
60
  end
@@ -0,0 +1,73 @@
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
+ puts "Calculating churn and complexity values for #{churn[:churn][:changes].size} #{type} files"
30
+
31
+ values = churn[:churn][:changes].map do |change|
32
+ history = git_history_for_file(file_path: change[:file_path])
33
+ commit = history&.first&.first
34
+
35
+ cached_value = Cache.read(file_path: change[:file_path])
36
+
37
+ if !cached_value.nil? && !cached_value.current_commit.nil? && cached_value.current_commit == commit
38
+ value = cached_value
39
+ else
40
+ complexity, details = yield(change)
41
+
42
+ value = Value.new(file_path: change[:file_path],
43
+ churn: change[:times_changed],
44
+ complexity: complexity,
45
+ details: details,
46
+ history: history)
47
+ Cache.write(file_path: change[:file_path], value: value)
48
+ end
49
+
50
+ print "."
51
+ value
52
+ end
53
+
54
+ Cache.persist!
55
+
56
+ print "\n\n"
57
+
58
+ values
59
+ end
60
+
61
+ private
62
+
63
+ def git_history_for_file(file_path:, limit: 10)
64
+ history = `git log --oneline -n #{limit} -- #{file_path}`
65
+ history.split("\n")
66
+ .map do |log_entry|
67
+ log_entry.partition(/\A(\S+)\s/)
68
+ .map(&:strip)
69
+ .reject(&:empty?)
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/attractor/cli.rb CHANGED
@@ -8,6 +8,7 @@ module Attractor
8
8
  # contains methods implementing the CLI
9
9
  class CLI < Thor
10
10
  shared_options = [[:file_prefix, aliases: :p],
11
+ [:verbose, aliases: :v, type: :boolean],
11
12
  [:ignore, aliases: :i, default: ""],
12
13
  [:watch, aliases: :w, type: :boolean],
13
14
  [:minimum_churn, aliases: :c, type: :numeric, default: 3],
@@ -86,7 +87,8 @@ module Attractor
86
87
  file_prefix: options[:file_prefix],
87
88
  minimum_churn_count: options[:minimum_churn],
88
89
  ignores: options[:ignore],
89
- start_ago: options[:start_ago])
90
+ start_ago: options[:start_ago],
91
+ verbose: options[:verbose])
90
92
  end
91
93
 
92
94
  def report!(reporter)
@@ -0,0 +1,101 @@
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 "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"
45
+ shared_options.each do |shared_option|
46
+ option(*shared_option)
47
+ end
48
+ option(:format, aliases: :f, default: :table)
49
+ def calc
50
+ file_prefix = options[:file_prefix]
51
+ output_format = options[:format]
52
+
53
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), format: output_format)
54
+ rescue RuntimeError => e
55
+ puts "Runtime error: #{e.message}"
56
+ end
57
+
58
+ desc "report", "Generates an HTML report"
59
+ (shared_options + advanced_options).each do |option|
60
+ option(*option)
61
+ end
62
+ def report
63
+ file_prefix = options[:file_prefix]
64
+ open_browser = !(options[:no_open_browser] || options[:ci])
65
+
66
+ report! Attractor::HtmlReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
67
+ rescue RuntimeError => e
68
+ puts "Runtime error: #{e.message}"
69
+ end
70
+
71
+ desc "serve", "Serves the report on localhost"
72
+ (shared_options + advanced_options).each do |option|
73
+ option(*option)
74
+ end
75
+ def serve
76
+ file_prefix = options[:file_prefix]
77
+ open_browser = !(options[:no_open_browser] || options[:ci])
78
+
79
+ report! Attractor::SinatraReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), open_browser: open_browser)
80
+ end
81
+
82
+ private
83
+
84
+ def calculators(options)
85
+ Attractor.calculators_for_type(options[:type],
86
+ file_prefix: options[:file_prefix],
87
+ minimum_churn_count: options[:minimum_churn],
88
+ ignores: options[:ignore],
89
+ start_ago: options[:start_ago])
90
+ end
91
+
92
+ def report!(reporter)
93
+ if options[:watch]
94
+ puts "Listening for file changes..."
95
+ reporter.watch
96
+ else
97
+ reporter.report
98
+ end
99
+ end
100
+ end
101
+ end
@@ -19,7 +19,7 @@ module Attractor
19
19
  @file_prefix = file_prefix || ""
20
20
  @calculators = calculators
21
21
  @open_browser = open_browser
22
- @suggester = Suggester.new(values)
22
+ @suggester = Suggester.new
23
23
 
24
24
  @watcher = Watcher.new(@file_prefix, ignores, lambda do
25
25
  report
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "descriptive_statistics/safe"
4
+ require "fileutils"
5
+ require "forwardable"
6
+ require "launchy"
7
+ require "tilt"
8
+
9
+ module Attractor
10
+ # base reporter
11
+ class BaseReporter
12
+ extend Forwardable
13
+ attr_accessor :file_prefix
14
+ attr_reader :types
15
+ attr_writer :values
16
+ def_delegator :@watcher, :watch
17
+
18
+ def initialize(calculators:, file_prefix: "", ignores: "", open_browser: true)
19
+ @file_prefix = file_prefix || ""
20
+ @calculators = calculators
21
+ @open_browser = open_browser
22
+ @suggester = Suggester.new(values)
23
+
24
+ @watcher = Watcher.new(@file_prefix, ignores, lambda do
25
+ report
26
+ end)
27
+ rescue NoMethodError => _e
28
+ raise "There was a problem gathering churn changes"
29
+ end
30
+
31
+ def suggestions(quantile:, type: "rb")
32
+ @suggester.values = values(type: type)
33
+ @suggestions = @suggester.suggest(quantile)
34
+ @suggestions
35
+ end
36
+
37
+ def report
38
+ @suggestions = @suggester.suggest
39
+ @types = @calculators.map { |calc| [calc.first, calc.last.type] }.to_h
40
+ end
41
+
42
+ def render
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}"
51
+ end
52
+ end
53
+ end
@@ -49,13 +49,40 @@ module Attractor
49
49
  end
50
50
  end
51
51
 
52
+ class JSONFormatter
53
+ def call(calculators)
54
+ result = calculators.map do |calc|
55
+ type = calc.last.type
56
+ values = calc.last.calculate
57
+ suggester = Suggester.new(values)
58
+ to_be_refactored = suggester.suggest.map(&:file_path)
59
+
60
+ [
61
+ type, values.map do |value|
62
+ {
63
+ file_path: value.file_path,
64
+ score: value.score,
65
+ complexity: value.complexity,
66
+ churn: value.churn,
67
+ refactor: to_be_refactored.include?(value.file_path)
68
+ }
69
+ end
70
+ ]
71
+ end
72
+
73
+ puts result.to_h.to_json
74
+ end
75
+ end
76
+
52
77
  def initialize(format:, **other)
53
78
  super(**other)
54
79
  @formatter = case format.to_sym
55
- when :csv
56
- CSVFormatter.new
57
- else
58
- TableFormatter.new
80
+ when :csv
81
+ CSVFormatter.new
82
+ when :json
83
+ JSONFormatter.new
84
+ else
85
+ TableFormatter.new
59
86
  end
60
87
  end
61
88
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attractor
4
+ # console reporter
5
+ class ConsoleReporter < BaseReporter
6
+ class TableFormatter
7
+ def call(calculators)
8
+ puts "Calculated churn and complexity"
9
+ puts
10
+ puts "file_path#{" " * 53}complexity churn"
11
+ puts "-" * 80
12
+
13
+ calculators.each do |calc|
14
+ # e.g. ['js', JsCalculator']
15
+ puts calc.last.type
16
+
17
+ values = calc.last.calculate
18
+ suggester = Suggester.new(values)
19
+
20
+ puts values&.map(&:to_s)
21
+ puts
22
+ puts "Suggestions for refactorings:"
23
+ suggester.suggest&.each { |sug| puts sug.file_path }
24
+ puts
25
+ end
26
+ end
27
+ end
28
+
29
+ class CSVFormatter
30
+ def call(calculators)
31
+ require "csv"
32
+
33
+ result = CSV.generate do |csv|
34
+ csv << %w[file_path score complexity churn type refactor]
35
+
36
+ calculators.each do |calc|
37
+ type = calc.last.type
38
+ values = calc.last.calculate
39
+ suggester = Suggester.new(values)
40
+ to_be_refactored = suggester.suggest.map(&:file_path)
41
+
42
+ values.each do |value|
43
+ csv << [value.file_path, value.score, value.complexity, value.churn, type, to_be_refactored.include?(value.file_path)]
44
+ end
45
+ end
46
+ end
47
+
48
+ puts result
49
+ end
50
+ end
51
+
52
+ def initialize(format:, **other)
53
+ super(**other)
54
+ @formatter = case format.to_sym
55
+ when :csv
56
+ CSVFormatter.new
57
+ else
58
+ TableFormatter.new
59
+ end
60
+ end
61
+
62
+ def report
63
+ super
64
+ @formatter.call(@calculators)
65
+ end
66
+ end
67
+ end
@@ -14,10 +14,10 @@ module Attractor
14
14
  FileUtils.mkdir_p "./attractor_output/images"
15
15
  FileUtils.mkdir_p "./attractor_output/javascripts"
16
16
 
17
- File.open("./attractor_output/images/attractor_logo.svg", "w") { |file| file.write(logo) }
18
- File.open("./attractor_output/images/attractor_favicon.png", "w") { |file| file.write(favicon) }
19
- File.open("./attractor_output/stylesheets/main.css", "w") { |file| file.write(css) }
20
- File.open("./attractor_output/javascripts/index.pack.js", "w") { |file| file.write(javascript_pack) }
17
+ File.write("./attractor_output/images/attractor_logo.svg", logo)
18
+ File.write("./attractor_output/images/attractor_favicon.png", favicon)
19
+ File.write("./attractor_output/stylesheets/main.css", css)
20
+ File.write("./attractor_output/javascripts/index.pack.js", javascript_pack)
21
21
 
22
22
  if @calculators.size > 1
23
23
  @calculators.each do |calc|
@@ -25,8 +25,8 @@ module Attractor
25
25
  suggester = Suggester.new(values(type: @short_type))
26
26
  @suggestions = suggester.suggest
27
27
 
28
- File.open("./attractor_output/javascripts/index.#{@short_type}.js", "w") { |file| file.write(javascript) }
29
- File.open("./attractor_output/index.#{@short_type}.html", "w") { |file| file.write(render) }
28
+ File.write("./attractor_output/javascripts/index.#{@short_type}.js", javascript)
29
+ File.write("./attractor_output/index.#{@short_type}.html", render)
30
30
  puts "Generated HTML report at #{File.expand_path "./attractor_output/"}/index.#{@short_type}.html"
31
31
  end
32
32
 
@@ -35,8 +35,8 @@ module Attractor
35
35
  puts "Opening browser window..."
36
36
  end
37
37
  else
38
- File.open("./attractor_output/javascripts/index.js", "w") { |file| file.write(javascript) }
39
- File.open("./attractor_output/index.html", "w") { |file| file.write(render) }
38
+ File.write("./attractor_output/javascripts/index.js", javascript)
39
+ File.write("./attractor_output/index.html", render)
40
40
  puts "Generated HTML report at #{File.expand_path "./attractor_output/index.html"}"
41
41
 
42
42
  if @open_browser
@@ -5,8 +5,8 @@ module Attractor
5
5
  class Suggester
6
6
  attr_accessor :values
7
7
 
8
- def initialize(values)
9
- @values = values || []
8
+ def initialize(values = [])
9
+ @values = values
10
10
  end
11
11
 
12
12
  def suggest(threshold = 95)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attractor
4
+ # makes suggestions for refactorings
5
+ class Suggester
6
+ attr_accessor :values
7
+
8
+ def initialize(values)
9
+ @values = values || []
10
+ end
11
+
12
+ def suggest(threshold = 95)
13
+ products = @values.map(&:score)
14
+ products.extend(DescriptiveStatistics)
15
+ quantile = products.percentile(threshold.to_i)
16
+
17
+ @values.select { |val| val.score > quantile }
18
+ .sort_by { |val| val.score }.reverse
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Attractor
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -1,7 +1,3 @@
1
1
  module Attractor
2
- <<<<<<< HEAD
3
2
  VERSION = "2.4.0"
4
- =======
5
- VERSION = "2.3.0"
6
- >>>>>>> 1b4d274a6c7f6634ac4b6c2627f5de500788cef4
7
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-31 00:00:00.000000000 Z
11
+ date: 2023-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: churn
@@ -156,28 +156,28 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 0.2.0
159
+ version: 0.3.0
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 0.2.0
166
+ version: 0.3.0
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: attractor-ruby
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 0.2.0
173
+ version: 0.3.0
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 0.2.0
180
+ version: 0.3.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: autoprefixer-rails
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -373,16 +373,21 @@ files:
373
373
  - lib/attractor.rb
374
374
  - lib/attractor/cache.rb
375
375
  - lib/attractor/calculators/base_calculator.rb
376
+ - lib/attractor/calculators/base_calculator.rb~
376
377
  - lib/attractor/cli.rb
378
+ - lib/attractor/cli.rb~
377
379
  - lib/attractor/detectors/base_detector.rb
378
380
  - lib/attractor/duration_parser.rb
379
381
  - lib/attractor/gem_names.rb
380
382
  - lib/attractor/registry_entry.rb
381
383
  - lib/attractor/reporters/base_reporter.rb
384
+ - lib/attractor/reporters/base_reporter.rb~
382
385
  - lib/attractor/reporters/console_reporter.rb
386
+ - lib/attractor/reporters/console_reporter.rb~
383
387
  - lib/attractor/reporters/html_reporter.rb
384
388
  - lib/attractor/reporters/sinatra_reporter.rb
385
389
  - lib/attractor/suggester.rb
390
+ - lib/attractor/suggester.rb~
386
391
  - lib/attractor/value.rb
387
392
  - lib/attractor/version.rb
388
393
  - lib/attractor/version.rb~
@@ -395,7 +400,7 @@ metadata:
395
400
  source_code_uri: https://github.com/julianrubisch/attractor
396
401
  bug_tracker_uri: https://github.com/julianrubisch/attractor/issues
397
402
  changelog_uri: https://github.com/julianrubisch/attractor/CHANGELOG.md
398
- post_install_message:
403
+ post_install_message:
399
404
  rdoc_options: []
400
405
  require_paths:
401
406
  - lib
@@ -410,8 +415,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
410
415
  - !ruby/object:Gem::Version
411
416
  version: '0'
412
417
  requirements: []
413
- rubygems_version: 3.1.4
414
- signing_key:
418
+ rubygems_version: 3.4.12
419
+ signing_key:
415
420
  specification_version: 4
416
421
  summary: Churn vs Complexity Chart Generator
417
422
  test_files: []