churnalizer 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 121e0c4906c450169d97de281a8642791ac17b5e
4
+ data.tar.gz: d13666dd80e932cee1d1556b4cb208a888ae383b
5
+ SHA512:
6
+ metadata.gz: c757caeedbc38724192ddf613faf96baf3de26b37e8efd1b6fa79f4c7680321973ee9e46737a4979f9cc9a62a9d74def9ac048d3dd6ba2a1e5cf7a3ae2d06e17
7
+ data.tar.gz: 02c79cb372325593c507009f805c54ddcd4c73ff22acff093c273536cbe64646b34f5840dbb22fc9918bea9ccc8e6e425649a29bc740a744699f4d9e6e03704e
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ *.swp
13
+ .DS_Store
14
+ .byebug_history
15
+ chart.html
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ Style/StringLiterals:
2
+ EnforcedStyle: double_quotes
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.0
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in churnalizer.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ churnalizer (0.1.0)
5
+ flog
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ byebug (10.0.0)
11
+ diff-lcs (1.3)
12
+ flog (4.6.2)
13
+ path_expander (~> 1.0)
14
+ ruby_parser (~> 3.1, > 3.1.0)
15
+ sexp_processor (~> 4.8)
16
+ path_expander (1.0.2)
17
+ rake (10.5.0)
18
+ rb-readline (0.5.5)
19
+ rspec (3.7.0)
20
+ rspec-core (~> 3.7.0)
21
+ rspec-expectations (~> 3.7.0)
22
+ rspec-mocks (~> 3.7.0)
23
+ rspec-core (3.7.1)
24
+ rspec-support (~> 3.7.0)
25
+ rspec-expectations (3.7.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-mocks (3.7.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.7.0)
31
+ rspec-support (3.7.1)
32
+ ruby_parser (3.11.0)
33
+ sexp_processor (~> 4.9)
34
+ sexp_processor (4.10.1)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ bundler (~> 1.16)
41
+ byebug
42
+ churnalizer!
43
+ rake (~> 10.0)
44
+ rb-readline
45
+ rspec (~> 3.0)
46
+
47
+ BUNDLED WITH
48
+ 1.16.1
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Federico Ramirez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Churnalizer
2
+
3
+ Churnalizer helps you analyze the churn vs complexity of your Ruby application.
4
+
5
+ ![Graph Screenshot](screenshot.png?raw=true)
6
+
7
+ What is churn vs complexity? Sandi Metz explains it nicely in her blog post
8
+ [Breaking up the
9
+ Behemot](https://www.sandimetz.com/blog/2017/9/13/breaking-up-the-behemoth).
10
+
11
+ Basically, it shows you which files need to be refactored first -- top-right
12
+ corner of the graph. Churn is how many times a file has been changed, so you
13
+ want files which change a lot to be simple. Files which are never touched are
14
+ fine with being complex for a while.
15
+
16
+ ## Installation
17
+
18
+ $ gem install churnalizer
19
+
20
+ ## Usage
21
+
22
+ $ churnalizer my-app-directory/
23
+
24
+ This was only tested on MacOS. It uses the `open` command to make things easier,
25
+ so when the gem is done analyzing your app, it will open the generated chart
26
+ with your default browser.
27
+
28
+ That functionality would not work on Linux so the chart would need to be opened
29
+ manually.
30
+
31
+ Don't think it works on Windows at all, given the churn counter uses the
32
+ following command: `cd $(dirname #{file}) && git log --oneline -- #{file} | wc -l`
33
+
34
+ ### Ignoring Files
35
+
36
+ By default, Churnalizer will ignore specs and Rails files like `schema.rb` and
37
+ `routes.rb`.
38
+
39
+ For custom ignore rules, in the base directory you are analyzing, create a file
40
+ named `.churnignore`, in that file, add a regular expression per-line to run
41
+ against file paths. If a regex returns true, it will be ignored.
42
+
43
+ For example, this is the default `.churnignore` file:
44
+
45
+ /test/
46
+ /spec/
47
+ /db/
48
+ /config/
49
+ /bin/
50
+ /vendor/
51
+ /public/
52
+
53
+ ## Development
54
+
55
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
56
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
57
+ prompt that will allow you to experiment.
58
+
59
+ To install this gem onto your local machine, run `bundle exec rake install`. To
60
+ release a new version, update the version number in `version.rb`, and then run
61
+ `bundle exec rake release`, which will create a git tag for the version, push
62
+ git commits and tags, and push the `.gem` file to
63
+ [rubygems.org](https://rubygems.org).
64
+
65
+ ### Playing with the CLI
66
+
67
+ To locally run the CLI use `ruby -Ilib exe/churnalizer`
68
+
69
+ ## Contributing
70
+
71
+ Bug reports and pull requests are welcome on GitHub at
72
+ https://github.com/gosukiwi/churnalizer.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "churnalizer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "churnalizer/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "churnalizer"
8
+ spec.version = Churnalizer::VERSION
9
+ spec.authors = ["Federico Ramirez"]
10
+ spec.email = ["federico_r@beezwax.net"]
11
+
12
+ spec.summary = %q{Analyze your Ruby application for Churn vs Complexity}
13
+ spec.homepage = "http://github.com/gosukiwi/churnalizer"
14
+
15
+ if spec.respond_to?(:metadata)
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ else
18
+ raise "RubyGems 2.0 or newer is required to protect against " \
19
+ "public gem pushes."
20
+ end
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.16"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "byebug"
33
+ spec.add_development_dependency "rb-readline"
34
+
35
+ spec.add_dependency "flog"
36
+ end
data/exe/churnalizer ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "churnalizer/cli"
4
+ Churnalizer::CLI.new
@@ -0,0 +1,22 @@
1
+ module ChurnAnalyzers
2
+ # This class uses Git to analyze the churn (how many times it was changed) of
3
+ # a file.
4
+ #
5
+ class Git
6
+ def analyze(file)
7
+ as_integer run_command(file)
8
+ end
9
+
10
+ private
11
+
12
+ # Trim string and cast to integer
13
+ def as_integer(string)
14
+ string.gsub(/[\n ]+/, "").to_i
15
+ end
16
+
17
+ # NOTE: This only works on *NIX systems
18
+ def run_command(file)
19
+ `cd $(dirname #{file}) && git log --oneline -- #{file} | wc -l`
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ require "file_scanners/ruby"
2
+ require "churn_analyzers/git"
3
+ require "complexity_analyzers/ruby"
4
+ require "graph_builders/google_charts"
5
+
6
+ module Churnalizer
7
+ class Analyzer
8
+ attr_reader :path
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def run
14
+ build_graph analyzed_files
15
+ end
16
+
17
+ private
18
+
19
+ def analyzed_files
20
+ files.map do |file|
21
+ [display_name_for(file), analyze(file)]
22
+ end.to_h
23
+ end
24
+
25
+ def display_name_for(file)
26
+ file.gsub(path, ".")
27
+ end
28
+
29
+ def analyze(file)
30
+ { churn: churn_for(file), complexity: complexity_for(file) }
31
+ end
32
+
33
+ def files
34
+ file_scanner.scan
35
+ end
36
+
37
+ def file_scanner
38
+ @file_scanner ||= FileScanners::Ruby.new(path)
39
+ end
40
+
41
+ def churn_analyzer
42
+ @churn_analyzer ||= ChurnAnalyzers::Git.new
43
+ end
44
+
45
+ def churn_for(file)
46
+ churn_analyzer.analyze(file)
47
+ end
48
+
49
+ def complexity_analyzer
50
+ @complexity_analyzer ||= ComplexityAnalyzers::Ruby.new
51
+ end
52
+
53
+ def complexity_for(file)
54
+ complexity_analyzer.analyze(file)
55
+ end
56
+
57
+ def graph_builder
58
+ @graph_builder ||= GraphBuilders::GoogleCharts.new
59
+ end
60
+
61
+ def build_graph(graph_data)
62
+ graph_builder.build(graph_data, save_to: "chart.html")
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,34 @@
1
+ require "churnalizer/analyzer"
2
+
3
+ module Churnalizer
4
+ class CLI
5
+ attr_accessor :path
6
+
7
+ def initialize
8
+ @path = path
9
+ analyze
10
+ end
11
+
12
+ def analyze
13
+ return help if path.nil?
14
+ puts churnalizer.run
15
+ end
16
+
17
+ def churnalizer
18
+ @churnalizer ||= Churnalizer::Analyzer.new(path)
19
+ end
20
+
21
+ def help
22
+ puts """This is Churnalizer, a churn vs complexity analyzer for your Ruby
23
+ application.
24
+
25
+ Usage:
26
+ churnalizer my-app/
27
+ """
28
+ end
29
+
30
+ def path
31
+ ARGV[0]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Churnalizer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "churnalizer/version"
2
+
3
+ module Churnalizer
4
+ # Silence is beautiful
5
+ end
@@ -0,0 +1,18 @@
1
+ require "flog"
2
+
3
+ module ComplexityAnalyzers
4
+ # This class uses Flog <https://github.com/seattlerb/flog> to check the
5
+ # complexity of a Ruby file.
6
+ #
7
+ class Ruby
8
+ def analyze(file)
9
+ flog.reset
10
+ flog.flog(file)
11
+ flog.total_score
12
+ end
13
+
14
+ def flog
15
+ @flog ||= Flog.new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ module FileScanners
2
+ # Takes a list of files and ignores them appropriately
3
+ #
4
+ class Ignorer
5
+ DEFAULT_IGNORES = %w[/test/ /spec/ /db/ /config/ /bin/ /vendor/ /public/].freeze
6
+
7
+ attr_reader :path
8
+ def initialize(path)
9
+ @path = path
10
+ end
11
+
12
+ def ignore?(file)
13
+ ignore_rules.each do |rule|
14
+ return true if rule === file
15
+ end
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def ignore_rules
22
+ @ignore_rules ||= churnignore.map { |regex| Regexp.new(regex) }
23
+ end
24
+
25
+ def churnignore
26
+ if churnignore_contents.empty?
27
+ DEFAULT_IGNORES
28
+ else
29
+ churnignore_as_array
30
+ end
31
+ end
32
+
33
+ def churnignore_as_array
34
+ churnignore_contents.split("\n").compact
35
+ end
36
+
37
+ def churnignore_contents
38
+ @churnignore_contents ||= File.read(churnignore_path)
39
+ rescue Errno::ENOENT
40
+ ""
41
+ end
42
+
43
+ def churnignore_path
44
+ "#{path}/.churnignore"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ require "file_scanners/ignorer"
2
+
3
+ module FileScanners
4
+ # FileScanners are in charge of finding files to be analyzed. This one in
5
+ # particular finds Ruby files.
6
+ #
7
+ class Ruby
8
+ attr_reader :path
9
+ def initialize(path)
10
+ @path = path
11
+ end
12
+
13
+ def scan
14
+ files.reject do |file|
15
+ ignore? file
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def files
22
+ Dir.glob("#{path}/**/*.rb")
23
+ end
24
+
25
+ def ignorer
26
+ @ignorer ||= Ignorer.new(path)
27
+ end
28
+
29
+ def ignore?(file)
30
+ ignorer.ignore? file
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ require "json"
2
+
3
+ module GraphBuilders
4
+ # Generate an HTML chart using Google Charts
5
+ #
6
+ class GoogleCharts
7
+ def build(graph_data, save_to:)
8
+ write_chart graph_data, to_file: save_to
9
+ open_with_default_browser save_to
10
+ File.expand_path save_to
11
+ end
12
+
13
+ private
14
+
15
+ def open_with_default_browser(file)
16
+ `open #{file}` if `which open` == "/usr/bin/open\n"
17
+ end
18
+
19
+ def write_chart(graph_data, to_file:)
20
+ contents = compile_template graph_data
21
+ File.write(to_file, contents)
22
+ end
23
+
24
+ def compile_template(graph_data)
25
+ template.gsub("{{graph_data}}", graph_data.to_json)
26
+ end
27
+
28
+ def template
29
+ File.read view_path("google_chart.html")
30
+ end
31
+
32
+ def view_path(name)
33
+ "#{__dir__}/views/#{name}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Churnalizer: Churn vs Complexity</title>
6
+ <style>
7
+ body {
8
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"
9
+ }
10
+ h1,
11
+ h2 {
12
+ text-align: center;
13
+ }
14
+
15
+ p {
16
+ text-align: center;
17
+ margin: 0 auto 1.25em;
18
+ max-width: 780px;
19
+ line-height: 1.2;
20
+ }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1>Churn vs Complexity</h1>
25
+
26
+ <div id="churnalizer-chart"></div>
27
+
28
+ <h2>How to read this chart?</h2>
29
+ <p>
30
+ Basically, you want to minimize the amount of files which show up at the
31
+ top-right of the chart. Those are files which are complex and also change
32
+ a lot.
33
+ </p>
34
+ <p>
35
+ Ideally, you don't want to have complex files, but if you do, at least make
36
+ sure they don't change often. The files at the top-right are good candidates
37
+ for refactoring.
38
+ </p>
39
+
40
+ <script src="https://www.gstatic.com/charts/loader.js"></script>
41
+ <script>
42
+ google.charts.load('current', {'packages':['corechart']});
43
+ google.charts.setOnLoadCallback(drawChart);
44
+
45
+ var graph_data = {{graph_data}};
46
+
47
+ function drawChart() {
48
+ var data = new google.visualization.DataTable();
49
+ data.addColumn('number', 'Churn');
50
+ data.addColumn('number', 'Complexity');
51
+ data.addColumn({ type: 'string', role: 'tooltip' });
52
+
53
+ for(var file in graph_data) {
54
+ file_data = graph_data[file]
55
+ data.addRow([
56
+ file_data.churn,
57
+ file_data.complexity,
58
+ file + "\nComplexity: " + Math.round(file_data.complexity) + "\nChurn: " + file_data.churn
59
+ ]);
60
+ }
61
+
62
+ var options = {
63
+ hAxis: {title: 'Churn', minValue: 0, maxValue: 50},
64
+ vAxis: {title: 'Complexity', minValue: 0, maxValue: 200},
65
+ legend: 'none',
66
+ crosshair: { trigger: 'both' }
67
+ };
68
+
69
+ var chart = new google.visualization.ScatterChart(document.getElementById('churnalizer-chart'));
70
+
71
+ chart.draw(data, options);
72
+ }
73
+ </script>
74
+ </body>
75
+ </html>
data/screenshot.png ADDED
Binary file