attractor 2.6.0 → 2.7.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.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attractor
4
+ module Formatters
5
+ class Formatter
6
+ def initialize(target:, format:)
7
+ @target = target
8
+ @format = format
9
+ end
10
+
11
+ def call(data)
12
+ @format.call(@target.call(data))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attractor
4
+ module Formatters
5
+ class Report
6
+ attr_reader :title, :metadata, :columns, :rows, :footer, :group_by, :rows_key
7
+
8
+ def initialize(title:, columns:, rows:, metadata: {}, footer: nil, group_by: nil, rows_key: :rows)
9
+ @title = title
10
+ @metadata = metadata
11
+ @columns = columns
12
+ @rows = rows
13
+ @footer = footer
14
+ @group_by = group_by
15
+ @rows_key = rows_key
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "attractor/formatters/report"
4
+
5
+ module Attractor
6
+ module Formatters
7
+ module Targets
8
+ class Console
9
+ def call(calculators)
10
+ rows = []
11
+ footer_lines = []
12
+
13
+ calculators.each do |type, calc|
14
+ values = calc.calculate
15
+ suggester = Suggester.new(values)
16
+ refactor_files = suggester.suggest.map(&:file_path)
17
+
18
+ values.each do |value|
19
+ rows << {
20
+ type: type,
21
+ file_path: value.file_path,
22
+ score: value.score,
23
+ complexity: value.complexity,
24
+ churn: value.churn,
25
+ refactor: refactor_files.include?(value.file_path),
26
+ details: value.details,
27
+ history: value.history
28
+ }
29
+ end
30
+
31
+ footer_lines << "Suggestions for refactorings:"
32
+ footer_lines += refactor_files unless refactor_files.empty?
33
+ end
34
+
35
+ Report.new(
36
+ title: "Calculated churn and complexity",
37
+ metadata: {schema: 2},
38
+ columns: %i[file_path score complexity churn type refactor],
39
+ rows: rows,
40
+ footer: footer_lines.empty? ? nil : footer_lines.join("\n"),
41
+ group_by: :type
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "attractor/formatters/report"
4
+
5
+ module Attractor
6
+ module Formatters
7
+ module Targets
8
+ class Diff
9
+ def call(data)
10
+ Report.new(
11
+ title: "Complexity diff between #{data[:base_ref]} and #{data[:head_ref]}",
12
+ metadata: {
13
+ total_score_base: data[:total_score_base],
14
+ total_score_head: data[:total_score_head],
15
+ trend: data[:trend]
16
+ },
17
+ columns: %i[file_path complexity_base complexity_head delta churn score_head refactor_base refactor_head],
18
+ rows: data[:files],
19
+ rows_key: :files
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+ require "rugged"
6
+
7
+ module Attractor
8
+ module Git
9
+ def self.repo
10
+ @repo ||= Rugged::Repository.discover(".")
11
+ end
12
+
13
+ def self.validate_ref!(ref)
14
+ repo.rev_parse(ref)
15
+ rescue Rugged::ReferenceError
16
+ raise ArgumentError, "Invalid git ref: #{ref}"
17
+ end
18
+
19
+ def self.diff_files(base_ref, head_ref)
20
+ base_commit = repo.merge_base(base_ref, head_ref)
21
+ head_commit = repo.rev_parse(head_ref)
22
+
23
+ repo.diff(base_commit, head_commit).deltas.map { |delta| delta.new_file[:path] }.uniq
24
+ end
25
+
26
+ class Worktree
27
+ attr_reader :path
28
+
29
+ def initialize(ref)
30
+ @ref = ref
31
+ @path = Dir.mktmpdir("attractor-diff-#{ref}-")
32
+ end
33
+
34
+ def checkout
35
+ success = system("git", "worktree", "add", "-f", @path, @ref, out: File::NULL, err: File::NULL)
36
+ raise "Failed to create git worktree for #{@ref} at #{@path}" unless success
37
+
38
+ self
39
+ end
40
+
41
+ def cleanup
42
+ system("git", "worktree", "remove", "-f", @path, out: File::NULL, err: File::NULL)
43
+ FileUtils.rm_rf(@path)
44
+ end
45
+
46
+ def chdir(&block)
47
+ Dir.chdir(@path, &block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -10,6 +10,7 @@ module Attractor
10
10
  # base reporter
11
11
  class BaseReporter
12
12
  extend Forwardable
13
+
13
14
  attr_accessor :file_prefix
14
15
  attr_reader :types
15
16
  attr_writer :values
@@ -1,96 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "attractor/formatters/formatter"
4
+ require "attractor/formatters/targets/console"
5
+ require "attractor/formatters/formats/json"
6
+ require "attractor/formatters/formats/table"
7
+ require "attractor/formatters/formats/csv"
8
+ require "attractor/formatters/format_strategy"
9
+
3
10
  module Attractor
4
11
  # console reporter
5
12
  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
- 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
- details: value.details,
69
- history: value.history
70
- }
71
- end
72
- ]
73
- end
74
-
75
- puts result.to_h.to_json
76
- end
77
- end
13
+ include Attractor::Formatters::FormatStrategy
78
14
 
79
15
  def initialize(format:, **other)
80
16
  super(**other)
81
- @formatter = case format.to_sym
82
- when :csv
83
- CSVFormatter.new
84
- when :json
85
- JSONFormatter.new
86
- else
87
- TableFormatter.new
88
- end
17
+ @formatter = Attractor::Formatters::Formatter.new(
18
+ target: Attractor::Formatters::Targets::Console.new,
19
+ format: format_strategy(format)
20
+ )
89
21
  end
90
22
 
91
23
  def report
92
24
  super
93
- @formatter.call(@calculators)
25
+ puts @formatter.call(@calculators)
94
26
  end
95
27
  end
96
28
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "attractor/formatters/formatter"
4
+ require "attractor/formatters/targets/diff"
5
+ require "attractor/formatters/formats/json"
6
+ require "attractor/formatters/formats/table"
7
+ require "attractor/formatters/formats/markdown"
8
+ require "attractor/formatters/format_strategy"
9
+
10
+ module Attractor
11
+ # Reporter for complexity deltas between two git refs
12
+ class DiffReporter
13
+ include Attractor::Formatters::FormatStrategy
14
+
15
+ def initialize(format:)
16
+ @formatter = Attractor::Formatters::Formatter.new(
17
+ target: Attractor::Formatters::Targets::Diff.new,
18
+ format: format_strategy(format)
19
+ )
20
+ end
21
+
22
+ def report(data)
23
+ puts @formatter.call(data)
24
+ end
25
+ end
26
+ end
@@ -10,11 +10,14 @@ module Attractor
10
10
  end
11
11
 
12
12
  def suggest(threshold = 95)
13
- products = @values.map(&:score)
13
+ scored_values = @values.reject { |value| value.score.nil? }
14
+ return [] if scored_values.empty?
15
+
16
+ products = scored_values.map(&:score)
14
17
  products.extend(DescriptiveStatistics)
15
18
  quantile = products.percentile(threshold.to_i)
16
19
 
17
- @values.select { |val| val.score > quantile }
20
+ scored_values.select { |val| val.score > quantile }
18
21
  .sort_by { |val| val.score }.reverse
19
22
  end
20
23
  end
@@ -20,11 +20,14 @@ module Attractor
20
20
  end
21
21
 
22
22
  def score
23
+ return nil if @complexity.nil?
24
+
23
25
  @complexity * @churn
24
26
  end
25
27
 
26
28
  def to_s
27
- format("%-64s%8.1f%8i", @file_path, @complexity, @churn)
29
+ complexity_str = @complexity.nil? ? "n/a" : format("%8.1f", @complexity)
30
+ format("%-64s%8s%8i", @file_path, complexity_str, @churn)
28
31
  end
29
32
 
30
33
  def to_h
@@ -1,3 +1,3 @@
1
1
  module Attractor
2
- VERSION = "2.6.0"
2
+ VERSION = "2.7.0"
3
3
  end
@@ -17,7 +17,7 @@ module Attractor
17
17
 
18
18
  listener = Listen.to(File.absolute_path(@file_prefix), ignore: ignore) do |modified, _added, _removed|
19
19
  if modified
20
- puts "#{modified.map(&:to_s).join(", ")} modified, recalculating..."
20
+ puts "#{modified.join(", ")} modified, recalculating..."
21
21
  @callback.call
22
22
  end
23
23
  end
data/lib/attractor.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "attractor/version"
4
4
  require "attractor/gem_names"
5
5
  require "attractor/duration_parser"
6
+ require "attractor/config"
6
7
  require "attractor/calculators/base_calculator"
7
8
  require "attractor/detectors/base_detector"
8
9
  require "attractor/reporters/base_reporter"
@@ -10,12 +11,6 @@ require "attractor/suggester"
10
11
  require "attractor/watcher"
11
12
  require "attractor/cache"
12
13
 
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
14
  module Attractor
20
15
  class Error < StandardError; end
21
16
 
@@ -35,11 +30,20 @@ module Attractor
35
30
  end
36
31
 
37
32
  def calculators_for_type(type, **options)
33
+ files = options.delete(:files)
38
34
  registry_entry_for_type = @registry_entries[type]
39
35
 
40
- return {type => registry_entry_for_type.calculator_class.new(**options)} if type
36
+ if type
37
+ calculator = registry_entry_for_type.calculator_class.new(**options)
38
+ calculator.files = files if calculator.respond_to?(:files=)
39
+ return {type => calculator}
40
+ end
41
41
 
42
- all_registered_calculators(**options)
42
+ all_registered_calculators(**options).tap do |calculators|
43
+ calculators.each_value do |calculator|
44
+ calculator.files = files if calculator.respond_to?(:files=)
45
+ end
46
+ end
43
47
  end
44
48
 
45
49
  def all_registered_calculators(options = {})
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-12-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: churn
@@ -24,6 +23,20 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: 1.0.4
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: descriptive_statistics
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +51,20 @@ dependencies:
38
51
  - - ">="
39
52
  - !ruby/object:Gem::Version
40
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: logger
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
41
68
  - !ruby/object:Gem::Dependency
42
69
  name: flog
43
70
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +107,20 @@ dependencies:
80
107
  - - "~>"
81
108
  - !ruby/object:Gem::Version
82
109
  version: '3.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ostruct
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
83
124
  - !ruby/object:Gem::Dependency
84
125
  name: rack-livereload
85
126
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +135,20 @@ dependencies:
94
135
  - - ">="
95
136
  - !ruby/object:Gem::Version
96
137
  version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rugged
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '1.0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '1.0'
97
152
  - !ruby/object:Gem::Dependency
98
153
  name: sinatra
99
154
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +225,14 @@ dependencies:
170
225
  requirements:
171
226
  - - "~>"
172
227
  - !ruby/object:Gem::Version
173
- version: 0.3.0
228
+ version: 0.4.0
174
229
  type: :development
175
230
  prerelease: false
176
231
  version_requirements: !ruby/object:Gem::Requirement
177
232
  requirements:
178
233
  - - "~>"
179
234
  - !ruby/object:Gem::Version
180
- version: 0.3.0
235
+ version: 0.4.0
181
236
  - !ruby/object:Gem::Dependency
182
237
  name: autoprefixer-rails
183
238
  requirement: !ruby/object:Gem::Requirement
@@ -376,14 +431,27 @@ files:
376
431
  - lib/attractor/calculators/base_calculator.rb~
377
432
  - lib/attractor/cli.rb
378
433
  - lib/attractor/cli.rb~
434
+ - lib/attractor/config.rb
379
435
  - lib/attractor/detectors/base_detector.rb
436
+ - lib/attractor/diff_calculator.rb
380
437
  - lib/attractor/duration_parser.rb
438
+ - lib/attractor/formatters/format_strategy.rb
439
+ - lib/attractor/formatters/formats/csv.rb
440
+ - lib/attractor/formatters/formats/json.rb
441
+ - lib/attractor/formatters/formats/markdown.rb
442
+ - lib/attractor/formatters/formats/table.rb
443
+ - lib/attractor/formatters/formatter.rb
444
+ - lib/attractor/formatters/report.rb
445
+ - lib/attractor/formatters/targets/console.rb
446
+ - lib/attractor/formatters/targets/diff.rb
381
447
  - lib/attractor/gem_names.rb
448
+ - lib/attractor/git.rb
382
449
  - lib/attractor/registry_entry.rb
383
450
  - lib/attractor/reporters/base_reporter.rb
384
451
  - lib/attractor/reporters/base_reporter.rb~
385
452
  - lib/attractor/reporters/console_reporter.rb
386
453
  - lib/attractor/reporters/console_reporter.rb~
454
+ - lib/attractor/reporters/diff_reporter.rb
387
455
  - lib/attractor/reporters/html_reporter.rb
388
456
  - lib/attractor/reporters/sinatra_reporter.rb
389
457
  - lib/attractor/suggester.rb
@@ -400,7 +468,6 @@ metadata:
400
468
  source_code_uri: https://github.com/julianrubisch/attractor
401
469
  bug_tracker_uri: https://github.com/julianrubisch/attractor/issues
402
470
  changelog_uri: https://github.com/julianrubisch/attractor/CHANGELOG.md
403
- post_install_message:
404
471
  rdoc_options: []
405
472
  require_paths:
406
473
  - lib
@@ -415,8 +482,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
415
482
  - !ruby/object:Gem::Version
416
483
  version: '0'
417
484
  requirements: []
418
- rubygems_version: 3.4.19
419
- signing_key:
485
+ rubygems_version: 4.0.10
420
486
  specification_version: 4
421
487
  summary: Churn vs Complexity Chart Generator
422
488
  test_files: []