attractor 2.3.0 → 2.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b116bbaa914ca032391378e04b8e41f27b28fa4feaca3fc50c93053d1954a15a
4
- data.tar.gz: 3e5620d661d19d3d2308457921389ce78b4ba3b844a6a4d3dd33c1ef9d093a56
3
+ metadata.gz: 1eef3d6e8104428fdc8240ae5161654990b53d5351962bc474f11e450db9cbc2
4
+ data.tar.gz: 6ec7109f7c6aa6e4b5b594863e5bfeff08652f2fdf6c8c2a86a208357c084713
5
5
  SHA512:
6
- metadata.gz: '092ff47f81f9d6f0e7fa025d21a89501413e1dc8c7044d5420d4db8ea04f72fc6554840a9656b0ac002c2f0c25ef3f0fa9c1204214d4b29a284eabb565428f82'
7
- data.tar.gz: 83ad8a7dafa969e68c4db77629663738513137a3f91ad058451d9a37c1ed39aff1cfea785c34042d543b7caac8ec625207cf3701c7003d63a23f631cbba109b5
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])
@@ -40,20 +41,20 @@ module Attractor
40
41
  complexity, details = yield(change)
41
42
 
42
43
  value = Value.new(file_path: change[:file_path],
43
- churn: change[:times_changed],
44
- complexity: complexity,
45
- details: details,
46
- history: history)
44
+ churn: change[:times_changed],
45
+ complexity: complexity,
46
+ details: details,
47
+ history: history)
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
@@ -26,7 +26,9 @@ module Attractor
26
26
  ignores: @ignores
27
27
  ).report(false)
28
28
 
29
- churn[:churn][:changes].map do |change|
29
+ puts "Calculating churn and complexity values for #{churn[:churn][:changes].size} #{type} files"
30
+
31
+ values = churn[:churn][:changes].map do |change|
30
32
  history = git_history_for_file(file_path: change[:file_path])
31
33
  commit = history&.first&.first
32
34
 
@@ -38,15 +40,22 @@ module Attractor
38
40
  complexity, details = yield(change)
39
41
 
40
42
  value = Value.new(file_path: change[:file_path],
41
- churn: change[:times_changed],
42
- complexity: complexity,
43
- details: details,
44
- history: history)
43
+ churn: change[:times_changed],
44
+ complexity: complexity,
45
+ details: details,
46
+ history: history)
45
47
  Cache.write(file_path: change[:file_path], value: value)
46
48
  end
47
49
 
50
+ print "."
48
51
  value
49
52
  end
53
+
54
+ Cache.persist!
55
+
56
+ print "\n\n"
57
+
58
+ values
50
59
  end
51
60
 
52
61
  private
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],
@@ -45,10 +46,12 @@ module Attractor
45
46
  shared_options.each do |shared_option|
46
47
  option(*shared_option)
47
48
  end
49
+ option(:format, aliases: :f, default: :table)
48
50
  def calc
49
51
  file_prefix = options[:file_prefix]
52
+ output_format = options[:format]
50
53
 
51
- report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options))
54
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), format: output_format)
52
55
  rescue RuntimeError => e
53
56
  puts "Runtime error: #{e.message}"
54
57
  end
@@ -84,7 +87,8 @@ module Attractor
84
87
  file_prefix: options[:file_prefix],
85
88
  minimum_churn_count: options[:minimum_churn],
86
89
  ignores: options[:ignore],
87
- start_ago: options[:start_ago])
90
+ start_ago: options[:start_ago],
91
+ verbose: options[:verbose])
88
92
  end
89
93
 
90
94
  def report!(reporter)
@@ -26,14 +26,31 @@ module Attractor
26
26
  puts "Runtime error: #{e.message}"
27
27
  end
28
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
+
29
44
  desc "calc", "Calculates churn and complexity for all ruby files in current directory"
30
45
  shared_options.each do |shared_option|
31
46
  option(*shared_option)
32
47
  end
48
+ option(:format, aliases: :f, default: :table)
33
49
  def calc
34
50
  file_prefix = options[:file_prefix]
51
+ output_format = options[:format]
35
52
 
36
- report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options))
53
+ report! Attractor::ConsoleReporter.new(file_prefix: file_prefix, ignores: options[:ignore], calculators: calculators(options), format: output_format)
37
54
  rescue RuntimeError => e
38
55
  puts "Runtime error: #{e.message}"
39
56
  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
@@ -36,7 +36,7 @@ module Attractor
36
36
 
37
37
  def report
38
38
  @suggestions = @suggester.suggest
39
- @types = Hash[@calculators.map { |calc| [calc.first, calc.last.type] }]
39
+ @types = @calculators.map { |calc| [calc.first, calc.last.type] }.to_h
40
40
  end
41
41
 
42
42
  def render
@@ -19,7 +19,6 @@ module Attractor
19
19
  @file_prefix = file_prefix || ""
20
20
  @calculators = calculators
21
21
  @open_browser = open_browser
22
- @values = @calculators.first.last.calculate
23
22
  @suggester = Suggester.new(values)
24
23
 
25
24
  @watcher = Watcher.new(@file_prefix, ignores, lambda do
@@ -37,7 +36,7 @@ module Attractor
37
36
 
38
37
  def report
39
38
  @suggestions = @suggester.suggest
40
- @types = Hash[@calculators.map { |calc| [calc.first, calc.last.type] }]
39
+ @types = @calculators.map { |calc| [calc.first, calc.last.type] }.to_h
41
40
  end
42
41
 
43
42
  def render
@@ -3,26 +3,92 @@
3
3
  module Attractor
4
4
  # console reporter
5
5
  class ConsoleReporter < BaseReporter
6
- def report
7
- super
8
- puts "Calculated churn and complexity"
9
- puts
10
- puts "file_path#{" " * 53}complexity churn"
11
- puts "-" * 80
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
12
 
13
- @calculators.each do |calc|
14
- # e.g. ['js', JsCalculator']
15
- puts calc.last.type
13
+ calculators.each do |calc|
14
+ # e.g. ['js', JsCalculator']
15
+ puts calc.last.type
16
16
 
17
- values = calc.last.calculate
18
- suggester = Suggester.new(values)
17
+ values = calc.last.calculate
18
+ suggester = Suggester.new(values)
19
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
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
25
49
  end
26
50
  end
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
+
77
+ def initialize(format:, **other)
78
+ super(**other)
79
+ @formatter = case format.to_sym
80
+ when :csv
81
+ CSVFormatter.new
82
+ when :json
83
+ JSONFormatter.new
84
+ else
85
+ TableFormatter.new
86
+ end
87
+ end
88
+
89
+ def report
90
+ super
91
+ @formatter.call(@calculators)
92
+ end
27
93
  end
28
94
  end
@@ -1,21 +1,67 @@
1
- require 'base_reporter'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Attractor
4
4
  # console reporter
5
5
  class ConsoleReporter < BaseReporter
6
- def report
7
- super
8
- puts 'Calculated churn and complexity'
9
- puts
10
- puts "file_path#{' ' * 53}complexity churn"
11
- puts '-' * 80
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]
12
35
 
13
- puts @values.map(&:to_s)
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)
14
41
 
15
- puts
16
- puts 'Suggestions for refactorings:'
17
- @suggestions.each { |sug| puts sug.file_path }
18
- puts
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)
19
65
  end
20
66
  end
21
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,17 +5,17 @@ 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)
13
- products = @values.map { |val| val.churn * val.complexity }
13
+ products = @values.map(&:score)
14
14
  products.extend(DescriptiveStatistics)
15
15
  quantile = products.percentile(threshold.to_i)
16
16
 
17
- @values.select { |val| val.churn * val.complexity > quantile }
18
- .sort_by { |val| val.churn * val.complexity }.reverse
17
+ @values.select { |val| val.score > quantile }
18
+ .sort_by { |val| val.score }.reverse
19
19
  end
20
20
  end
21
21
  end
@@ -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
@@ -19,6 +19,10 @@ module Attractor
19
19
  history&.first&.first
20
20
  end
21
21
 
22
+ def score
23
+ @complexity * @churn
24
+ end
25
+
22
26
  def to_s
23
27
  format("%-64s%8.1f%8i", @file_path, @complexity, @churn)
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Attractor
2
- VERSION = "2.3.0"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -0,0 +1,3 @@
1
+ module Attractor
2
+ VERSION = "2.4.0"
3
+ end
data/lib/attractor.rb CHANGED
@@ -43,9 +43,9 @@ module Attractor
43
43
  end
44
44
 
45
45
  def all_registered_calculators(options = {})
46
- Hash[@registry_entries.map do |type, entry|
46
+ @registry_entries.map do |type, entry|
47
47
  [type, entry.calculator_class.new(**options)] if entry.detector_class.new.detect
48
- end.compact]
48
+ end.compact.to_h
49
49
  end
50
50
 
51
51
  module_function :calculators_for_type
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.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-12 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
@@ -371,35 +371,27 @@ files:
371
371
  - app/views/index.html.erb
372
372
  - exe/attractor
373
373
  - lib/attractor.rb
374
- - lib/attractor.rb~
375
374
  - lib/attractor/cache.rb
376
- - lib/attractor/cache.rb~
377
375
  - lib/attractor/calculators/base_calculator.rb
378
376
  - lib/attractor/calculators/base_calculator.rb~
379
377
  - lib/attractor/cli.rb
380
378
  - lib/attractor/cli.rb~
381
379
  - lib/attractor/detectors/base_detector.rb
382
- - lib/attractor/detectors/base_detector.rb~
383
380
  - lib/attractor/duration_parser.rb
384
- - lib/attractor/duration_parser.rb~
385
381
  - lib/attractor/gem_names.rb
386
- - lib/attractor/gem_names.rb~
387
382
  - lib/attractor/registry_entry.rb
388
- - lib/attractor/registry_entry.rb~
389
383
  - lib/attractor/reporters/base_reporter.rb
390
384
  - lib/attractor/reporters/base_reporter.rb~
391
385
  - lib/attractor/reporters/console_reporter.rb
392
386
  - lib/attractor/reporters/console_reporter.rb~
393
387
  - lib/attractor/reporters/html_reporter.rb
394
- - lib/attractor/reporters/html_reporter.rb~
395
388
  - lib/attractor/reporters/sinatra_reporter.rb
396
389
  - lib/attractor/suggester.rb
397
- - lib/attractor/tmp/churn/9f86bc9b7ec6bf59cbf7a111ce8b1159ae128347.json
390
+ - lib/attractor/suggester.rb~
398
391
  - lib/attractor/value.rb
399
- - lib/attractor/value.rb~
400
392
  - lib/attractor/version.rb
393
+ - lib/attractor/version.rb~
401
394
  - lib/attractor/watcher.rb
402
- - lib/attractor/watcher.rb~
403
395
  homepage: https://github.com/julianrubisch/attractor
404
396
  licenses:
405
397
  - MIT
@@ -423,7 +415,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
423
415
  - !ruby/object:Gem::Version
424
416
  version: '0'
425
417
  requirements: []
426
- rubygems_version: 3.2.3
418
+ rubygems_version: 3.4.12
427
419
  signing_key:
428
420
  specification_version: 4
429
421
  summary: Churn vs Complexity Chart Generator
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "psych"
5
-
6
- module Attractor
7
- class Cache
8
- class << self
9
- def read(file_path:)
10
- adapter.read(file_path: file_path)
11
- end
12
-
13
- def write(file_path:, value:)
14
- adapter.write(file_path: file_path, value: value)
15
- end
16
-
17
- private
18
-
19
- def adapter
20
- @@adapter ||= CacheAdapter::JSON.instance
21
- end
22
- end
23
- end
24
-
25
- module CacheAdapter
26
- class Base
27
- include Singleton
28
- end
29
-
30
- class JSON < Base
31
- def initialize
32
- super
33
-
34
- @data_directory = "tmp"
35
- FileUtils.mkdir_p @data_directory
36
- FileUtils.touch filename
37
-
38
- begin
39
- @store = ::JSON.parse(File.read(filename))
40
- rescue ::JSON::ParserError
41
- @store = {}
42
- end
43
- end
44
-
45
- def read(file_path:)
46
- value_hash = @store[file_path]
47
-
48
- Value.new(**value_hash.values.first.transform_keys(&:to_sym)) unless value_hash.nil?
49
- rescue ArgumentError => e
50
- puts "Couldn't rehydrate value from cache: #{e.message}"
51
- nil
52
- end
53
-
54
- def write(file_path:, value:)
55
- mappings = {x: :churn, y: :complexity}
56
-
57
- transformed_value = value.to_h.transform_keys { |k| mappings[k] || k }
58
- @store[file_path] = {value.current_commit => transformed_value}
59
- File.write(filename, ::JSON.dump(@store))
60
- end
61
-
62
- def filename
63
- "#{@data_directory}/attractor-cache.json"
64
- end
65
- end
66
- end
67
- end
@@ -1,4 +0,0 @@
1
- module Attractor
2
- class BaseDetector
3
- end
4
- end
@@ -1,27 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -1,2 +0,0 @@
1
- RegistryEntry = Struct.new :type, :detector, :calculator
2
-
@@ -1,48 +0,0 @@
1
- module Attractor
2
- # HTML reporter
3
- class HtmlReporter < BaseReporter
4
- def report
5
- super
6
-
7
- puts 'Generating an HTML report'
8
- @serve_static = true
9
-
10
- FileUtils.mkdir_p './attractor_output'
11
- FileUtils.mkdir_p './attractor_output/stylesheets'
12
- FileUtils.mkdir_p './attractor_output/images'
13
- FileUtils.mkdir_p './attractor_output/javascripts'
14
-
15
- File.open('./attractor_output/images/attractor_logo.svg', 'w') { |file| file.write(logo) }
16
- File.open('./attractor_output/stylesheets/main.css', 'w') { |file| file.write(css) }
17
- File.open('./attractor_output/javascripts/index.js', 'w') { |file| file.write(javascript) }
18
- File.open('./attractor_output/javascripts/index.pack.js', 'w') { |file| file.write(javascript_pack) }
19
-
20
- File.open('./attractor_output/index.html', 'w') { |file| file.write(render) }
21
- puts "Generated HTML report at #{File.expand_path './attractor_output/index.html'}"
22
-
23
- Launchy.open(File.expand_path('./attractor_output/index.html'))
24
- end
25
-
26
- def logo
27
- File.read(File.expand_path('../../../app/assets/images/attractor_logo.svg', __dir__))
28
- end
29
-
30
- def css
31
- File.read(File.expand_path('../../../app/assets/stylesheets/main.css', __dir__))
32
- end
33
-
34
- def javascript_pack
35
- File.read(File.expand_path('../../../app/assets/javascripts/index.pack.js', __dir__))
36
- end
37
-
38
- def javascript
39
- template = Tilt.new(File.expand_path('../../../app/assets/javascripts/index.js.erb', __dir__))
40
- template.render self
41
- end
42
-
43
- def render
44
- template = Tilt.new(File.expand_path('../../../app/views/index.html.erb', __dir__))
45
- template.render self
46
- end
47
- end
48
- end
@@ -1 +0,0 @@
1
- {"churn":{"changes":[{"file_path":"app/assets/javascripts/index.pack.js","times_changed":21},{"file_path":"src/javascript/functions.js","times_changed":16},{"file_path":"src/javascript/index.js","times_changed":4}],"class_churn":[],"method_churn":[],"changed_files":[".gitignore","Rakefile","attractor.gemspec","lib/attractor.rb","lib/attractor/calculators/js_calculator.rb","lib/attractor/cli.rb","src/javascript/calculator/index.js","src/javascript/calculator/package-lock.json","src/javascript/calculator/package.json","src/javascript/calculator/webpack.config.js"],"changed_classes":[],"changed_methods":[]}}
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Attractor
6
- # holds a churn/complexity value
7
- class Value
8
- attr_reader :file_path, :churn, :complexity, :details, :history
9
-
10
- def initialize(file_path: "", churn: 1, complexity: 0, details: [], history: [])
11
- @file_path = file_path
12
- @churn = churn
13
- @complexity = complexity
14
- @details = details
15
- @history = history
16
- end
17
-
18
- def to_s
19
- format("%-64s%8.1f%8i", @file_path, @complexity, @churn)
20
- end
21
-
22
- def to_h
23
- {file_path: file_path, x: churn, y: complexity, details: details, history: history}
24
- end
25
-
26
- def to_json(_opt)
27
- to_h.to_json
28
- end
29
- end
30
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Attractor
4
- # functionality for watching file system changes
5
- class Watcher
6
- def initialize(file_prefix, callback)
7
- @file_prefix = file_prefix
8
- @callback = callback
9
- end
10
-
11
- def watch
12
- @callback.call
13
-
14
- listener = Listen.to(@file_prefix, ignore: /^attractor_output/) do |modified, _added, _removed|
15
- if modified
16
- puts "#{modified.map(&:to_s).join(', ')} modified, recalculating..."
17
- @callback.call
18
- end
19
- end
20
- listener.start
21
- sleep
22
- end
23
- end
24
- end
data/lib/attractor.rb~ DELETED
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "attractor/version"
4
- require "attractor/gem_names"
5
- require "attractor/duration_parser"
6
- require "attractor/calculators/base_calculator"
7
- require "attractor/detectors/base_detector"
8
- require "attractor/reporters/base_reporter"
9
- require "attractor/suggester"
10
- require "attractor/watcher"
11
- require "attractor/cache"
12
-
13
- Dir[File.join(__dir__, "attractor", "reporters", "*.rb")].sort.each do |file|
14
- next if file.start_with?("base")
15
-
16
- require file
17
- end
18
-
19
- module Attractor
20
- class Error < StandardError; end
21
-
22
- @registry_entries = {}
23
-
24
- def register(registry_entry)
25
- @registry_entries[registry_entry.type] = registry_entry
26
- end
27
-
28
- def calculators_for_type(type, **options)
29
- registry_entry_for_type = @registry_entries[type]
30
-
31
- return {type => registry_entry_for_type.calculator_class.new(**options)} if type
32
-
33
- Hash[@registry_entries.map do |type, entry|
34
- [type, entry.calculator_class.new(**options)] if entry.detector_class.new.detect
35
- end.compact]
36
- end
37
-
38
- module_function :calculators_for_type
39
- module_function :register
40
- end
41
-
42
- Attractor::GemNames.new.to_a.each do |gem_name|
43
- require "attractor/#{gem_name}"
44
- end