attractor 1.0.2 → 2.0.1

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.
data/attractor.gemspec CHANGED
@@ -28,7 +28,10 @@ Gem::Specification.new do |spec|
28
28
  # Specify which files should be added to the gem when it is released.
29
29
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
30
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(src|tmp|test|spec|features)/}) }
31
+ `git ls-files -z`.split("\x0").reject do |f|
32
+ f.match(%r{^(src|tmp|test|spec|features|\.github)/}) ||
33
+ %w[.all-contributorsrc .rspec .rspec_status .travis.yml].include?(f)
34
+ end
32
35
  end
33
36
  spec.bindir = 'exe'
34
37
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -45,6 +48,8 @@ Gem::Specification.new do |spec|
45
48
  spec.add_dependency 'tilt'
46
49
 
47
50
  spec.add_development_dependency 'aruba'
51
+ spec.add_development_dependency 'attractor-javascript'
52
+ spec.add_development_dependency 'attractor-ruby'
48
53
  spec.add_development_dependency 'autoprefixer-rails'
49
54
  spec.add_development_dependency 'bootstrap', '~> 4.3.1'
50
55
  spec.add_development_dependency 'bundler', '~> 2.0'
@@ -55,5 +60,6 @@ Gem::Specification.new do |spec|
55
60
  spec.add_development_dependency 'rake', '~> 10.0'
56
61
  spec.add_development_dependency 'rspec', '~> 3.0'
57
62
  spec.add_development_dependency 'sassc'
63
+ spec.add_development_dependency 'standard'
58
64
  spec.add_development_dependency 'structured_changelog'
59
65
  end
data/bin/test ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ echo "== Running Specs =="
4
+ bundle exec rspec
5
+ echo "== Running Cucumber Features =="
6
+ bundle exec cucumber
data/lib/attractor.rb CHANGED
@@ -1,29 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'attractor/version'
4
+ require 'attractor/gem_names'
5
+ require 'attractor/duration_parser'
4
6
  require 'attractor/calculators/base_calculator'
5
- require 'attractor/calculators/ruby_calculator'
6
- require 'attractor/calculators/js_calculator'
7
+ require 'attractor/detectors/base_detector'
7
8
  require 'attractor/reporters/base_reporter'
8
- require 'attractor/reporters/console_reporter'
9
- require 'attractor/reporters/html_reporter'
10
- require 'attractor/reporters/sinatra_reporter'
11
9
  require 'attractor/suggester'
12
10
  require 'attractor/watcher'
13
11
 
12
+ Dir[File.join(__dir__, 'attractor', 'reporters', '*.rb')].each do |file|
13
+ next if file.start_with?('base')
14
+
15
+ require file
16
+ end
17
+
14
18
  module Attractor
15
19
  class Error < StandardError; end
16
20
 
17
- def calculators_for_type(type, file_prefix)
18
- case type
19
- when 'js'
20
- { 'js' => JsCalculator.new(file_prefix: file_prefix) }
21
- when 'rb'
22
- { 'rb' => RubyCalculator.new(file_prefix: file_prefix) }
23
- else
24
- { 'rb' => RubyCalculator.new(file_prefix: file_prefix), 'js' => JsCalculator.new(file_prefix: file_prefix)}
25
- end
21
+ @registry_entries = {}
22
+
23
+ def register(registry_entry)
24
+ @registry_entries[registry_entry.type] = registry_entry
25
+ end
26
+
27
+ def calculators_for_type(type, **options)
28
+ registry_entry_for_type = @registry_entries[type]
29
+
30
+ return { type => registry_entry_for_type.calculator_class.new(**options) } if type
31
+
32
+
33
+ Hash[@registry_entries.map do |type, entry|
34
+ [type, entry.calculator_class.new(**options)] if entry.detector_class.new.detect
35
+ end]
26
36
  end
27
37
 
28
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}"
29
44
  end
@@ -7,10 +7,13 @@ require 'attractor/value'
7
7
  module Attractor
8
8
  # calculates churn and complexity
9
9
  class BaseCalculator
10
- def initialize(file_prefix: '', file_extension: 'rb', minimum_churn_count: 3)
10
+ attr_reader :type
11
+
12
+ def initialize(file_prefix: '', file_extension: 'rb', minimum_churn_count: 3, start_ago: '5y')
11
13
  @file_prefix = file_prefix
12
14
  @file_extension = file_extension
13
15
  @minimum_churn_count = minimum_churn_count
16
+ @start_date = Date.today - Attractor::DurationParser.new(start_ago).duration
14
17
  end
15
18
 
16
19
  def calculate
@@ -18,7 +21,7 @@ module Attractor
18
21
  file_extension: @file_extension,
19
22
  file_prefix: @file_prefix,
20
23
  minimum_churn_count: @minimum_churn_count,
21
- start_date: Date.today - 365 * 5
24
+ start_date: @start_date
22
25
  ).report(false)
23
26
 
24
27
  churn[:churn][:changes].map do |change|
data/lib/attractor/cli.rb CHANGED
@@ -7,43 +7,56 @@ require 'attractor'
7
7
  module Attractor
8
8
  # contains methods implementing the CLI
9
9
  class CLI < Thor
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]]
15
+
16
+ advanced_options = [[:format, aliases: :f, default: 'html'],
17
+ [:no_open_browser, type: :boolean],
18
+ [:ci, type: :boolean]]
19
+
20
+ desc "version", "Prints Attractor's version information"
21
+ map %w(-v --version) => :version
22
+ def version
23
+ puts "Attractor version #{Attractor::VERSION}"
24
+ rescue RuntimeError => e
25
+ puts "Runtime error: #{e.message}"
26
+ end
27
+
10
28
  desc 'calc', 'Calculates churn and complexity for all ruby files in current directory'
11
- option :file_prefix, aliases: :p
12
- option :watch, aliases: :w, type: :boolean
13
- option :type, aliases: :t
29
+ shared_options.each do |shared_option|
30
+ option(*shared_option)
31
+ end
14
32
  def calc
15
33
  file_prefix = options[:file_prefix]
16
- calculators = Attractor.calculators_for_type(options[:type], file_prefix)
17
34
  if options[:watch]
18
35
  puts 'Listening for file changes...'
19
- Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators).watch
36
+ Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators(options)).watch
20
37
  else
21
- Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators).report
38
+ Attractor::ConsoleReporter.new(file_prefix: file_prefix, calculators: calculators(options)).report
22
39
  end
23
40
  rescue RuntimeError => e
24
41
  puts "Runtime error: #{e.message}"
25
42
  end
26
43
 
27
44
  desc 'report', 'Generates an HTML report'
28
- option :format, aliases: :f, default: 'html'
29
- option :file_prefix, aliases: :p
30
- option :watch, aliases: :w, type: :boolean
31
- option :type, aliases: :t
32
- option :no_open_browser, type: :boolean
33
- option :ci, type: :boolean
45
+ (shared_options + advanced_options).each do |option|
46
+ option(*option)
47
+ end
34
48
  def report
35
49
  file_prefix = options[:file_prefix]
36
- calculators = Attractor.calculators_for_type(options[:type], file_prefix)
37
- open_browser = !(options[:no_open_browser] || options[:ci])
50
+ open_browser = !(options[:no_open_browser] || options[:ci])
38
51
  if options[:watch]
39
52
  puts 'Listening for file changes...'
40
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).watch
53
+ Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).watch
41
54
  else
42
55
  case options[:format]
43
56
  when 'html'
44
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).report
57
+ Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
45
58
  else
46
- Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).report
59
+ Attractor::HtmlReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
47
60
  end
48
61
  end
49
62
  rescue RuntimeError => e
@@ -51,27 +64,32 @@ module Attractor
51
64
  end
52
65
 
53
66
  desc 'serve', 'Serves the report on localhost'
54
- option :format, aliases: :f, default: 'html'
55
- option :file_prefix, aliases: :p
56
- option :watch, aliases: :w, type: :boolean
57
- option :type, aliases: :t
58
- option :no_open_browser, type: :boolean
59
- option :ci, type: :boolean
67
+ (shared_options + advanced_options).each do |option|
68
+ option(*option)
69
+ end
60
70
  def serve
61
71
  file_prefix = options[:file_prefix]
62
- open_browser = !(options[:no_open_browser] || options[:ci])
63
- calculators = Attractor.calculators_for_type(options[:type], file_prefix)
72
+ open_browser = !(options[:no_open_browser] || options[:ci])
64
73
  if options[:watch]
65
74
  puts 'Listening for file changes...'
66
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).watch
75
+ Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).watch
67
76
  else
68
77
  case options[:format]
69
78
  when 'html'
70
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).report
79
+ Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
71
80
  else
72
- Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators, open_browser: open_browser).report
81
+ Attractor::SinatraReporter.new(file_prefix: file_prefix, calculators: calculators(options), open_browser: open_browser).report
73
82
  end
74
83
  end
75
84
  end
85
+
86
+ private
87
+
88
+ def calculators(options)
89
+ 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])
93
+ end
76
94
  end
77
95
  end
@@ -0,0 +1,7 @@
1
+ module Attractor
2
+ class BaseDetector
3
+ def detect
4
+ raise NotImplementedError
5
+ end
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,16 @@
1
+ module Attractor
2
+ # from https://github.com/prontolabs/pronto/blob/master/lib/pronto/gem_names.rb
3
+ class GemNames
4
+ def to_a
5
+ gems.map { |gem| gem.name.sub(/^attractor-/, '') }.uniq.sort
6
+ end
7
+
8
+ private
9
+
10
+ def gems
11
+ Gem::Specification.find_all.select do |gem|
12
+ gem.name =~ /^attractor-/
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Attractor
2
+ class RegistryEntry
3
+ attr_reader :type, :detector_class, :calculator_class
4
+
5
+ def initialize(type:, detector_class:, calculator_class:)
6
+ @type = type
7
+ @detector_class = detector_class
8
+ @calculator_class = calculator_class
9
+ end
10
+ end
11
+ end
12
+
@@ -10,6 +10,7 @@ module Attractor
10
10
  class BaseReporter
11
11
  extend Forwardable
12
12
  attr_accessor :values, :file_prefix
13
+ attr_reader :types
13
14
  def_delegator :@watcher, :watch
14
15
 
15
16
  def initialize(file_prefix: '', calculators:, open_browser: true)
@@ -33,6 +34,7 @@ module Attractor
33
34
 
34
35
  def report
35
36
  @suggestions = @suggester.suggest
37
+ @types = Hash[@calculators.map { |calc| [calc.first, calc.last.type] }]
36
38
  end
37
39
 
38
40
  def render
@@ -10,12 +10,19 @@ module Attractor
10
10
  puts "file_path#{' ' * 53}complexity churn"
11
11
  puts '-' * 80
12
12
 
13
- puts @values&.map(&:to_s)
13
+ @calculators.each do |calc|
14
+ # e.g. ['js', JsCalculator']
15
+ puts calc.last.type
14
16
 
15
- puts
16
- puts 'Suggestions for refactorings:'
17
- @suggestions&.each { |sug| puts sug.file_path }
18
- puts
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
19
26
  end
20
27
  end
21
28
  end
@@ -15,23 +15,47 @@ module Attractor
15
15
  FileUtils.mkdir_p './attractor_output/javascripts'
16
16
 
17
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) }
18
19
  File.open('./attractor_output/stylesheets/main.css', 'w') { |file| file.write(css) }
19
- File.open('./attractor_output/javascripts/index.js', 'w') { |file| file.write(javascript) }
20
20
  File.open('./attractor_output/javascripts/index.pack.js', 'w') { |file| file.write(javascript_pack) }
21
21
 
22
- File.open('./attractor_output/index.html', 'w') { |file| file.write(render) }
23
- puts "Generated HTML report at #{File.expand_path './attractor_output/index.html'}"
22
+ if @calculators.size > 1
23
+ @calculators.each do |calc|
24
+ @short_type = calc.first
25
+ @values = calc.last.calculate
26
+ suggester = Suggester.new(values)
27
+ @suggestions = suggester.suggest
24
28
 
25
- if @open_browser
26
- Launchy.open(File.expand_path('./attractor_output/index.html'))
27
- puts "Opening browser window..."
29
+ File.open("./attractor_output/javascripts/index.#{@short_type}.js", 'w') { |file| file.write(javascript) }
30
+ File.open("./attractor_output/index.#{@short_type}.html", 'w') { |file| file.write(render) }
31
+ puts "Generated HTML report at #{File.expand_path './attractor_output/'}/index.#{@short_type}.html"
32
+ end
33
+
34
+ if @open_browser
35
+ Launchy.open(File.expand_path("./attractor_output/index.#{@calculators.first.first}.html"))
36
+ puts "Opening browser window..."
37
+ end
38
+ else
39
+ File.open('./attractor_output/javascripts/index.js', 'w') { |file| file.write(javascript) }
40
+ File.open('./attractor_output/index.html', 'w') { |file| file.write(render) }
41
+ puts "Generated HTML report at #{File.expand_path './attractor_output/index.html'}"
42
+
43
+ if @open_browser
44
+ Launchy.open(File.expand_path('./attractor_output/index.html'))
45
+ puts "Opening browser window..."
46
+ end
28
47
  end
48
+
29
49
  end
30
50
 
31
51
  def logo
32
52
  File.read(File.expand_path('../../../app/assets/images/attractor_logo.svg', __dir__))
33
53
  end
34
54
 
55
+ def favicon
56
+ File.read(File.expand_path('../../../app/assets/images/attractor_favicon.png', __dir__))
57
+ end
58
+
35
59
  def css
36
60
  File.read(File.expand_path('../../../app/assets/stylesheets/main.css', __dir__))
37
61
  end
@@ -17,6 +17,7 @@ module Attractor
17
17
  set :show_exceptions, :after_handler
18
18
 
19
19
  get '/' do
20
+ @types = @reporter.types
20
21
  erb File.read(File.expand_path('../../../app/views/index.html.erb', __dir__))
21
22
  end
22
23
 
@@ -73,6 +74,8 @@ module Attractor
73
74
  def values(type: 'rb')
74
75
  @values = @calculators[type].calculate
75
76
  @values
77
+ rescue NoMethodError => e
78
+ puts "No calculator for type #{type}"
76
79
  end
77
80
  end
78
81
  end
@@ -4,7 +4,7 @@ module Attractor
4
4
  # makes suggestions for refactorings
5
5
  class Suggester
6
6
  def initialize(values)
7
- @values = values
7
+ @values = values || []
8
8
  end
9
9
 
10
10
  def suggest(threshold = 95)
@@ -1,3 +1,3 @@
1
1
  module Attractor
2
- VERSION = "1.0.2"
2
+ VERSION = "2.0.1"
3
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: 1.0.2
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-14 00:00:00.000000000 Z
11
+ date: 2020-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: churn
@@ -150,6 +150,34 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: attractor-javascript
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: attractor-ruby
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: autoprefixer-rails
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -290,6 +318,20 @@ dependencies:
290
318
  - - ">="
291
319
  - !ruby/object:Gem::Version
292
320
  version: '0'
321
+ - !ruby/object:Gem::Dependency
322
+ name: standard
323
+ requirement: !ruby/object:Gem::Requirement
324
+ requirements:
325
+ - - ">="
326
+ - !ruby/object:Gem::Version
327
+ version: '0'
328
+ type: :development
329
+ prerelease: false
330
+ version_requirements: !ruby/object:Gem::Requirement
331
+ requirements:
332
+ - - ">="
333
+ - !ruby/object:Gem::Version
334
+ version: '0'
293
335
  - !ruby/object:Gem::Dependency
294
336
  name: structured_changelog
295
337
  requirement: !ruby/object:Gem::Requirement
@@ -318,17 +360,15 @@ extra_rdoc_files: []
318
360
  files:
319
361
  - ".babelrc"
320
362
  - ".eslintrc.json"
321
- - ".github/workflows/ruby.yml"
322
363
  - ".gitignore"
323
- - ".rspec"
324
364
  - ".rubocop.yml"
325
- - ".travis.yml"
326
365
  - CHANGELOG.md
327
366
  - Gemfile
328
367
  - Guardfile
329
368
  - LICENSE
330
369
  - README.md
331
370
  - Rakefile
371
+ - app/assets/images/attractor_favicon.png
332
372
  - app/assets/images/attractor_logo.png
333
373
  - app/assets/images/attractor_logo.svg
334
374
  - app/assets/images/attractor_logo@2x.png
@@ -339,13 +379,16 @@ files:
339
379
  - attractor.gemspec
340
380
  - bin/console
341
381
  - bin/setup
382
+ - bin/test
342
383
  - dist/calculator.bundle.js
343
384
  - exe/attractor
344
385
  - lib/attractor.rb
345
386
  - lib/attractor/calculators/base_calculator.rb
346
- - lib/attractor/calculators/js_calculator.rb
347
- - lib/attractor/calculators/ruby_calculator.rb
348
387
  - lib/attractor/cli.rb
388
+ - lib/attractor/detectors/base_detector.rb
389
+ - lib/attractor/duration_parser.rb
390
+ - lib/attractor/gem_names.rb
391
+ - lib/attractor/registry_entry.rb
349
392
  - lib/attractor/reporters/.keep
350
393
  - lib/attractor/reporters/base_reporter.rb
351
394
  - lib/attractor/reporters/console_reporter.rb