churn_vs_complexity 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.devcontainer/devcontainer.json +26 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +3 -0
- data/Dockerfile +34 -0
- data/Dockerfile.dev +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +6 -0
- data/bin/churn_vs_complexity +11 -0
- data/lib/churn_vs_complexity/churn.rb +34 -0
- data/lib/churn_vs_complexity/cli.rb +79 -0
- data/lib/churn_vs_complexity/complexity/flog_calculator.rb +21 -0
- data/lib/churn_vs_complexity/complexity/pmd_calculator.rb +58 -0
- data/lib/churn_vs_complexity/complexity.rb +9 -0
- data/lib/churn_vs_complexity/concurrent_calculator.rb +60 -0
- data/lib/churn_vs_complexity/config.rb +70 -0
- data/lib/churn_vs_complexity/engine.rb +21 -0
- data/lib/churn_vs_complexity/file_selector.rb +46 -0
- data/lib/churn_vs_complexity/serializer.rb +36 -0
- data/lib/churn_vs_complexity/version.rb +5 -0
- data/lib/churn_vs_complexity.rb +18 -0
- data/tmp/pmd-support/ruleset.xml +21 -0
- data/tmp/template/graph.html +77 -0
- data/tmp/test-support/java/small-example/src/main/java/org/example/Main.java +10 -0
- data/tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java +28 -0
- data/tmp/test-support/txt/abc.txt +0 -0
- data/tmp/test-support/txt/d.txt +0 -0
- data/tmp/test-support/txt/ef.txt +0 -0
- data/tmp/test-support/txt/ghij.txt +0 -0
- data/tmp/test-support/txt/klm.txt +0 -0
- data/tmp/test-support/txt/nopq.txt +0 -0
- data/tmp/test-support/txt/r.txt +0 -0
- data/tmp/test-support/txt/st.txt +0 -0
- data/tmp/test-support/txt/uvx.txt +0 -0
- data/tmp/test-support/txt/yz.txt +0 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2efc73253aa94e1fe4de49723e2d7238857aa1a1f0b37fa6c39cb3e6cd1a133a
|
4
|
+
data.tar.gz: '09be5e6af924ce5d32cc81e918f88e943f13e3c80e78bffaeb799c957d300c78'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 72fb9a5556e813e7999ca5134e91566e9c2b64e9050076c170578db470cb98c68bd53c440fb2b15b68db60abddab9af40026723deb543cf44b6a83acaeeed83e
|
7
|
+
data.tar.gz: 17adf306ab68cf12ed73b733f2810df28761f726077cd6825ddb393fa89cf1fa264c66fd98eccd8c7bdfafb24af953e04f8b05a276615a2c0ae8b6c59be1c02b
|
@@ -0,0 +1,26 @@
|
|
1
|
+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
2
|
+
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
|
3
|
+
{
|
4
|
+
"name": "Existing Dockerfile",
|
5
|
+
"build": {
|
6
|
+
// Sets the run context to one level up instead of the .devcontainer folder.
|
7
|
+
"context": "..",
|
8
|
+
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
|
9
|
+
"dockerfile": "../Dockerfile.dev"
|
10
|
+
}
|
11
|
+
|
12
|
+
// Features to add to the dev container. More info: https://containers.dev/features.
|
13
|
+
// "features": {},
|
14
|
+
|
15
|
+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
16
|
+
// "forwardPorts": [],
|
17
|
+
|
18
|
+
// Uncomment the next line to run commands after the container is created.
|
19
|
+
// "postCreateCommand": "cat /etc/os-release",
|
20
|
+
|
21
|
+
// Configure tool-specific properties.
|
22
|
+
// "customizations": {},
|
23
|
+
|
24
|
+
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
|
25
|
+
// "remoteUser": "devcontainer"
|
26
|
+
}
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.3
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
Layout/LineLength:
|
6
|
+
Max: 120
|
7
|
+
|
8
|
+
|
9
|
+
Style/Documentation:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Style/TrailingCommaInArrayLiteral:
|
13
|
+
Enabled: true
|
14
|
+
EnforcedStyleForMultiline: consistent_comma
|
15
|
+
|
16
|
+
Style/TrailingCommaInHashLiteral:
|
17
|
+
Enabled: true
|
18
|
+
EnforcedStyleForMultiline: consistent_comma
|
19
|
+
|
20
|
+
Style/TrailingCommaInArguments:
|
21
|
+
Enabled: true
|
22
|
+
EnforcedStyleForMultiline: consistent_comma
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Create a build image with wget and unzip
|
2
|
+
FROM ubuntu:latest as pmd_builder
|
3
|
+
# Install wget and unzip
|
4
|
+
RUN apt-get update && \
|
5
|
+
apt-get install -y --no-install-recommends \
|
6
|
+
wget unzip
|
7
|
+
|
8
|
+
RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
|
9
|
+
RUN unzip pmd-dist-7.0.0-bin.zip
|
10
|
+
|
11
|
+
FROM ruby:3.3-bullseye
|
12
|
+
# Install git and java JDK
|
13
|
+
|
14
|
+
RUN apt-get update; \
|
15
|
+
apt-get install -y --no-install-recommends \
|
16
|
+
git openjdk-17-jdk
|
17
|
+
|
18
|
+
# copy PMD from build image
|
19
|
+
COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
|
20
|
+
# make pmd available in the PATH
|
21
|
+
RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
|
22
|
+
|
23
|
+
COPY *.gemspec Gemfile* /app/
|
24
|
+
COPY lib /app/lib
|
25
|
+
|
26
|
+
WORKDIR /app
|
27
|
+
|
28
|
+
RUN bundle install
|
29
|
+
|
30
|
+
COPY bin /app/bin
|
31
|
+
COPY tmp/pmd-support /app/tmp/pmd-support
|
32
|
+
COPY tmp/template /app/tmp/template
|
33
|
+
|
34
|
+
CMD ["/bin/sh", "-c", "echo 'It works!'"]
|
data/Dockerfile.dev
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Create a build image with wget and unzip
|
2
|
+
FROM ubuntu:latest as pmd_builder
|
3
|
+
# Install wget and unzip
|
4
|
+
RUN apt-get update && \
|
5
|
+
apt-get install -y --no-install-recommends \
|
6
|
+
wget unzip
|
7
|
+
|
8
|
+
RUN wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip --no-check-certificate
|
9
|
+
RUN unzip pmd-dist-7.0.0-bin.zip
|
10
|
+
|
11
|
+
FROM ruby:3.3-bullseye
|
12
|
+
# Install git and java JDK
|
13
|
+
|
14
|
+
RUN apt-get update; \
|
15
|
+
apt-get install -y --no-install-recommends \
|
16
|
+
git openjdk-17-jdk
|
17
|
+
|
18
|
+
# copy PMD from build image
|
19
|
+
COPY --from=pmd_builder /pmd-bin-7.0.0 /usr/local/bin/pmd-bin-7.0.0
|
20
|
+
# make pmd available in the PATH
|
21
|
+
RUN ln -s /usr/local/bin/pmd-bin-7.0.0/bin/pmd /usr/local/bin/pmd
|
22
|
+
|
23
|
+
COPY *.gemspec Gemfile* ./
|
24
|
+
COPY lib/churn_vs_complexity/version.rb lib/churn_vs_complexity/version.rb
|
25
|
+
RUN bundle install
|
26
|
+
|
27
|
+
CMD ["/bin/sh", "-c", "echo 'It works!'"]
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Erik T. Madsen
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# ChurnVsComplexity
|
2
|
+
|
3
|
+
A tool to visualise code complexity in a project and help direct refactoring efforts.
|
4
|
+
|
5
|
+
Inspired by [Michael Feathers' article "Getting Empirical about Refactoring"](https://www.agileconnection.com/article/getting-empirical-about-refactoring) and the gem [turbulence](https://rubygems.org/gems/turbulence) by Chad Fowler and others.
|
6
|
+
|
7
|
+
This gem was built primarily to support analysis of Java and Ruby repositories, but it can easily be extended.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'churn_vs_complexity'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install churn_vs_complexity
|
24
|
+
|
25
|
+
This gem depends on git for churn analysis and [PMD](https://pmd.github.io) for complexity analysis of JVM based languages.
|
26
|
+
|
27
|
+
In order to use the `--java` flag, you must first install PMD manually, and the gem assumes it is available on the search path as `pmd`. On macOS, for example, you can install it using homebrew with `brew install pmd`.
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Execute the `churn_vs_complexity` with the applicable arguments. Output in the requested format will be directed to stdout.
|
32
|
+
|
33
|
+
```
|
34
|
+
churn_vs_complexity [options] folder
|
35
|
+
--java Check complexity of java classes
|
36
|
+
--ruby Check complexity of ruby files
|
37
|
+
--csv Format output as CSV
|
38
|
+
--graph Format output as HTML page with Churn vs Complexity graph
|
39
|
+
--excluded PATTERN Exclude file paths including this string. Can be used multiple times.
|
40
|
+
--since YYYY-MM-DD Calculate churn after this date
|
41
|
+
-h, --help Display help
|
42
|
+
```
|
43
|
+
|
44
|
+
## Examples
|
45
|
+
|
46
|
+
`churn_vs_complexity --ruby --csv my_ruby_project > ~/Desktop/ruby-demo.csv`
|
47
|
+
|
48
|
+
`churn_vs_complexity --java --graph --exclude generated-sources --exclude generated-test-sources --since 2023-01-01 my_java_project > ~/Desktop/java-demo.html`
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
## Development
|
53
|
+
|
54
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
55
|
+
|
56
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/beatmadsen/churn_vs_complexity.
|
61
|
+
|
62
|
+
## License
|
63
|
+
|
64
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'git'
|
4
|
+
|
5
|
+
module ChurnVsComplexity
|
6
|
+
module Churn
|
7
|
+
module GitCalculator
|
8
|
+
class << self
|
9
|
+
def calculate(folder:, file:, since:)
|
10
|
+
with_follow = calculate_with_follow(folder, file, since)
|
11
|
+
with_follow.zero? ? repo(folder).log.path(file).size : with_follow
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def calculate_with_follow(folder, file, since)
|
17
|
+
# Format the date as "YYYY-MM-DD"
|
18
|
+
formatted_date = since.strftime('%Y-%m-%d')
|
19
|
+
# git log --follow --oneline --since="YYYY-MM-DD" <file_path> | wc -l
|
20
|
+
`git --git-dir #{File.join(folder,
|
21
|
+
'.git',)} --work-tree #{folder} log --follow --oneline --since=#{formatted_date} #{file} | wc -l`.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def repo(folder)
|
25
|
+
repos[folder] ||= Git.open(folder)
|
26
|
+
end
|
27
|
+
|
28
|
+
def repos
|
29
|
+
@repos ||= {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module ChurnVsComplexity
|
8
|
+
class CLI
|
9
|
+
def self.run!
|
10
|
+
# Create an options hash to store parsed options
|
11
|
+
options = { excluded: [] }
|
12
|
+
since = nil
|
13
|
+
|
14
|
+
# Initialize OptionParser
|
15
|
+
OptionParser.new do |opts|
|
16
|
+
opts.banner = 'Usage: churn_vs_complexity [options] folder'
|
17
|
+
|
18
|
+
opts.on('--java', 'Check complexity of java classes') do
|
19
|
+
options[:language] = :java
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('--ruby', 'Check complexity of ruby files') do
|
23
|
+
options[:language] = :ruby
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('--csv', 'Format output as CSV') do
|
27
|
+
options[:serializer] = :csv
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('--graph', 'Format output as HTML page with Churn vs Complexity graph') do
|
31
|
+
options[:serializer] = :graph
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('--excluded PATTERN',
|
35
|
+
'Exclude file paths including this string. Can be used multiple times.',) do |value|
|
36
|
+
options[:excluded] << value
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('--since YYYY-MM-DD', 'Calculate churn after this date') do |value|
|
40
|
+
since = value
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-h', '--help', 'Display help') do
|
44
|
+
puts opts
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
end.parse!
|
48
|
+
|
49
|
+
# First argument that is not an option is the folder
|
50
|
+
folder = ARGV.first
|
51
|
+
|
52
|
+
raise Error, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
|
53
|
+
|
54
|
+
# Verify that folder exists
|
55
|
+
raise Error, "Folder #{folder} does not exist" unless File.directory?(folder)
|
56
|
+
|
57
|
+
raise Error, 'No options selected. Use --help for usage information.' if options.empty?
|
58
|
+
|
59
|
+
begin
|
60
|
+
if since.nil?
|
61
|
+
since = Time.at(0).to_date
|
62
|
+
options[:graph_title] = 'Churn vs Complexity'
|
63
|
+
else
|
64
|
+
date_string = since
|
65
|
+
since = Date.strptime(since, '%Y-%m-%d')
|
66
|
+
options[:graph_title] = "Churn vs Complexity since #{date_string}"
|
67
|
+
end
|
68
|
+
rescue StandardError
|
69
|
+
raise Error, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
|
70
|
+
end
|
71
|
+
|
72
|
+
config = Config.new(**options)
|
73
|
+
|
74
|
+
config.validate!
|
75
|
+
|
76
|
+
puts config.to_engine.check(folder:, since:)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'flog'
|
4
|
+
|
5
|
+
module ChurnVsComplexity
|
6
|
+
module Complexity
|
7
|
+
module FlogCalculator
|
8
|
+
CONCURRENCY = Etc.nprocessors
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def folder_based? = false
|
12
|
+
|
13
|
+
def calculate(file:)
|
14
|
+
flog = Flog.new
|
15
|
+
flog.flog(file)
|
16
|
+
{ file => flog.total_score }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Complexity
|
5
|
+
module PMDCalculator
|
6
|
+
CONCURRENCY = Etc.nprocessors
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def folder_based? = true
|
10
|
+
|
11
|
+
def calculate(folder:)
|
12
|
+
output = `pmd check -d #{folder} -R #{resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{resolve_cache_path}`
|
13
|
+
Parser.new.parse(output)
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_dependencies!
|
17
|
+
`pmd --help`
|
18
|
+
rescue StandardError
|
19
|
+
raise Error, 'Could not execute PMD using command pmd'
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def resolve_ruleset_path
|
25
|
+
ruleset_path = File.join(gem_root, 'tmp', 'pmd-support', 'ruleset.xml')
|
26
|
+
raise "ruleset.xml not found in #{ruleset_path}" unless File.exist?(ruleset_path)
|
27
|
+
|
28
|
+
ruleset_path
|
29
|
+
end
|
30
|
+
|
31
|
+
def resolve_cache_path
|
32
|
+
File.join(gem_root, 'tmp', 'pmd-support', 'pmd-cache')
|
33
|
+
end
|
34
|
+
|
35
|
+
def gem_root
|
36
|
+
File.expand_path('../../..', __dir__)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Parser
|
41
|
+
def parse(output)
|
42
|
+
doc = JSON.parse(output)
|
43
|
+
doc['files'].each_with_object({}) do |file, result|
|
44
|
+
result[file['filename']] =
|
45
|
+
file['violations'].sum { |violation| extract_complexity(violation) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def extract_complexity(violation)
|
52
|
+
# Find text 'total cyclomatic complexity of <number>'
|
53
|
+
violation['description'].match(/total cyclomatic complexity of (\d+)/)[1].to_i
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
class ConcurrentCalculator
|
5
|
+
CONCURRENCY = Etc.nprocessors
|
6
|
+
|
7
|
+
def initialize(complexity:, churn:)
|
8
|
+
@complexity = complexity
|
9
|
+
@churn = churn
|
10
|
+
@churn_results = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def calculate(folder:, files:, since:)
|
14
|
+
schedule_churn_calculation(folder, files, since)
|
15
|
+
calculate_complexity(folder, files)
|
16
|
+
await_results
|
17
|
+
combine_results
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def calculate_complexity(folder, files)
|
23
|
+
@complexity_results =
|
24
|
+
if @complexity.folder_based?
|
25
|
+
@complexity.calculate(folder:)
|
26
|
+
else
|
27
|
+
files.each_with_object({}) do |file, acc|
|
28
|
+
acc.merge!(@complexity.calculate(file:))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def schedule_churn_calculation(folder, files, since)
|
34
|
+
f = files.dup
|
35
|
+
@threads = CONCURRENCY.times.map do
|
36
|
+
t = Thread.new do
|
37
|
+
until f.empty?
|
38
|
+
next_file = f.pop
|
39
|
+
@churn_results[next_file] = @churn.calculate(folder:, file: next_file, since:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
t.report_on_exception = false
|
43
|
+
t
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def await_results
|
48
|
+
@threads.each(&:join)
|
49
|
+
rescue StandardError => e
|
50
|
+
raise Error, "Failed to caculate churn: #{e.message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def combine_results
|
54
|
+
@churn_results.to_h do |file, churn|
|
55
|
+
complexity = @complexity_results[file] || -1
|
56
|
+
[file, [churn, complexity]]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
class Config
|
5
|
+
def initialize(
|
6
|
+
language:,
|
7
|
+
serializer:,
|
8
|
+
excluded: [],
|
9
|
+
graph_title: nil,
|
10
|
+
complexity_validator: ComplexityValidator
|
11
|
+
)
|
12
|
+
@language = language
|
13
|
+
@serializer = serializer
|
14
|
+
@excluded = excluded
|
15
|
+
@complexity_validator = complexity_validator
|
16
|
+
@graph_title = graph_title
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate!
|
20
|
+
raise Error, "Unsupported language: #{@language}" unless %i[java ruby].include?(@language)
|
21
|
+
raise Error, "Unsupported serializer: #{@serializer}" unless %i[none csv graph].include?(@serializer)
|
22
|
+
raise Error, 'Please provide a title for the graph' if @serializer == :graph && @graph_title.nil?
|
23
|
+
|
24
|
+
@complexity_validator.validate!(@language)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_engine
|
28
|
+
case @language
|
29
|
+
when :java
|
30
|
+
Engine.concurrent(
|
31
|
+
complexity: Complexity::PMDCalculator,
|
32
|
+
churn:,
|
33
|
+
file_selector: FileSelector::Java.excluding(@excluded),
|
34
|
+
serializer:,
|
35
|
+
)
|
36
|
+
when :ruby
|
37
|
+
Engine.concurrent(
|
38
|
+
complexity: Complexity::FlogCalculator,
|
39
|
+
churn:,
|
40
|
+
file_selector: FileSelector::Ruby.excluding(@excluded),
|
41
|
+
serializer:,
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def churn = Churn::GitCalculator
|
49
|
+
|
50
|
+
def serializer
|
51
|
+
case @serializer
|
52
|
+
when :none
|
53
|
+
Serializer::None
|
54
|
+
when :csv
|
55
|
+
Serializer::CSV
|
56
|
+
when :graph
|
57
|
+
Serializer::Graph.new(title: @graph_title)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ComplexityValidator
|
62
|
+
def self.validate!(language)
|
63
|
+
case language
|
64
|
+
when :java
|
65
|
+
Complexity::PMDCalculator.check_dependencies!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
class Engine
|
5
|
+
def initialize(file_selector:, calculator:, serializer:)
|
6
|
+
@file_selector = file_selector
|
7
|
+
@calculator = calculator
|
8
|
+
@serializer = serializer
|
9
|
+
end
|
10
|
+
|
11
|
+
def check(folder:, since:)
|
12
|
+
files = @file_selector.select_files(folder)
|
13
|
+
result = @calculator.calculate(folder:, files:, since:)
|
14
|
+
@serializer.serialize(result)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.concurrent(complexity:, churn:, serializer: Serializer::None, file_selector: FileSelector::Any)
|
18
|
+
Engine.new(file_selector:, serializer:, calculator: ConcurrentCalculator.new(complexity:, churn:))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module FileSelector
|
5
|
+
module Any
|
6
|
+
def self.select_files(folder)
|
7
|
+
Dir.glob("#{folder}/**/*").select { |f| File.file?(f) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Excluding
|
12
|
+
def initialize(extensions, excluded)
|
13
|
+
@extensions = extensions
|
14
|
+
@excluded = excluded
|
15
|
+
end
|
16
|
+
|
17
|
+
def select_files(folder)
|
18
|
+
Dir.glob("#{folder}/**/*").select do |f|
|
19
|
+
!has_excluded_pattern?(f) && has_correct_extension?(f) && File.file?(f)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def has_correct_extension?(file_path)
|
26
|
+
@extensions.any? { |e| file_path.end_with?(e) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_excluded_pattern?(file_path)
|
30
|
+
@excluded.any? { |e| file_path.include?(e) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module Java
|
35
|
+
def self.excluding(excluded)
|
36
|
+
Excluding.new(['.java'], excluded)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Ruby
|
41
|
+
def self.excluding(excluded)
|
42
|
+
Excluding.new(['.rb'], excluded)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Serializer
|
5
|
+
module None
|
6
|
+
def self.serialize(values_by_file) = values_by_file
|
7
|
+
end
|
8
|
+
|
9
|
+
module CSV
|
10
|
+
def self.serialize(values_by_file)
|
11
|
+
values_by_file.map do |file, values|
|
12
|
+
"#{file},#{values[0]},#{values[1]}\n"
|
13
|
+
end.join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Graph
|
18
|
+
def initialize(title:, template: Graph.load_template_file)
|
19
|
+
@template = template
|
20
|
+
@title = title
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize(values_by_file)
|
24
|
+
data = values_by_file.map do |file, values|
|
25
|
+
"{ file_path: '#{file}', churn: #{values[0]}, complexity: #{values[1]} }"
|
26
|
+
end.join(",\n") + "\n"
|
27
|
+
@template.gsub("// INSERT DATA\n", data).gsub('INSERT TITLE', @title)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.load_template_file
|
31
|
+
file_path = File.expand_path('../../tmp/template/graph.html', __dir__)
|
32
|
+
File.read(file_path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'etc'
|
5
|
+
|
6
|
+
require_relative 'churn_vs_complexity/version'
|
7
|
+
require_relative 'churn_vs_complexity/engine'
|
8
|
+
require_relative 'churn_vs_complexity/concurrent_calculator'
|
9
|
+
require_relative 'churn_vs_complexity/file_selector'
|
10
|
+
require_relative 'churn_vs_complexity/complexity'
|
11
|
+
require_relative 'churn_vs_complexity/churn'
|
12
|
+
require_relative 'churn_vs_complexity/cli'
|
13
|
+
require_relative 'churn_vs_complexity/config'
|
14
|
+
require_relative 'churn_vs_complexity/serializer'
|
15
|
+
|
16
|
+
module ChurnVsComplexity
|
17
|
+
class Error < StandardError; end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
|
3
|
+
<ruleset name="Rules for churn vs complexity"
|
4
|
+
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
5
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
6
|
+
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
7
|
+
|
8
|
+
<description>
|
9
|
+
Find cyclomatic complexity in java files
|
10
|
+
</description>
|
11
|
+
|
12
|
+
<rule ref="category/java/design.xml/CyclomaticComplexity">
|
13
|
+
<properties>
|
14
|
+
<property name="classReportLevel" value="1"/>
|
15
|
+
<!-- We only want one report pr class -->
|
16
|
+
<property name="methodReportLevel" value="1000000"/>
|
17
|
+
<property name="cycloOptions" value=""/>
|
18
|
+
</properties>
|
19
|
+
</rule>
|
20
|
+
|
21
|
+
</ruleset>
|
@@ -0,0 +1,77 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>INSERT TITLE</title>
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<canvas id="churnComplexityChart" width="800" height="400"></canvas>
|
11
|
+
|
12
|
+
<script>
|
13
|
+
// Example data points from your Ruby gem
|
14
|
+
const dataPoints = [
|
15
|
+
// INSERT DATA
|
16
|
+
];
|
17
|
+
|
18
|
+
// Extract data for Chart.js
|
19
|
+
const labels = dataPoints.map(point => point.file_path);
|
20
|
+
const churnData = dataPoints.map(point => point.churn);
|
21
|
+
const complexityData = dataPoints.map(point => point.complexity);
|
22
|
+
|
23
|
+
// Prepare data in Chart.js format
|
24
|
+
const data = {
|
25
|
+
labels: labels,
|
26
|
+
datasets: [{
|
27
|
+
label: 'INSERT TITLE',
|
28
|
+
data: dataPoints.map(point => ({ x: point.churn, y: point.complexity })),
|
29
|
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
30
|
+
borderColor: 'rgba(75, 192, 192, 1)',
|
31
|
+
borderWidth: 1,
|
32
|
+
pointRadius: 5,
|
33
|
+
pointHoverRadius: 10
|
34
|
+
}]
|
35
|
+
};
|
36
|
+
|
37
|
+
// Configure and render the chart
|
38
|
+
const config = {
|
39
|
+
type: 'scatter',
|
40
|
+
data: data,
|
41
|
+
options: {
|
42
|
+
scales: {
|
43
|
+
x: {
|
44
|
+
type: 'linear',
|
45
|
+
position: 'bottom',
|
46
|
+
title: {
|
47
|
+
display: true,
|
48
|
+
text: 'Churn'
|
49
|
+
}
|
50
|
+
},
|
51
|
+
y: {
|
52
|
+
title: {
|
53
|
+
display: true,
|
54
|
+
text: 'Complexity'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
plugins: {
|
59
|
+
tooltip: {
|
60
|
+
callbacks: {
|
61
|
+
label: function(context) {
|
62
|
+
const index = context.dataIndex;
|
63
|
+
return `${labels[index]}: Churn(${context.raw.x}), Complexity(${context.raw.y})`;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
const myChart = new Chart(
|
72
|
+
document.getElementById('churnComplexityChart'),
|
73
|
+
config
|
74
|
+
);
|
75
|
+
</script>
|
76
|
+
</body>
|
77
|
+
</html>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
package org.example.spice;
|
2
|
+
|
3
|
+
import java.util.Iterator;
|
4
|
+
import java.util.PrimitiveIterator;
|
5
|
+
import java.util.Random;
|
6
|
+
|
7
|
+
public class Checker {
|
8
|
+
public void check() {
|
9
|
+
for (int j = 0; j < 3; j++) {
|
10
|
+
var iter = tenRandomInts();
|
11
|
+
while (iter.hasNext()) {
|
12
|
+
var i = iter.nextInt();
|
13
|
+
if (i < 100) {
|
14
|
+
System.out.println("Bongo");
|
15
|
+
} else if (i > 200) {
|
16
|
+
System.out.println("Dingo");
|
17
|
+
} else {
|
18
|
+
System.out.println("Zapp");
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
private static PrimitiveIterator.OfInt tenRandomInts() {
|
25
|
+
var randomInts = new Random().ints();
|
26
|
+
return randomInts.limit(10).iterator();
|
27
|
+
}
|
28
|
+
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: churn_vs_complexity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Erik T. Madsen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: flog
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: git
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.1'
|
41
|
+
description: Inspired by Michael Feathers' article "Getting Empirical about Refactoring"
|
42
|
+
and the gem 'turbulence' by Chad Fowler and others. This gem was built primarily
|
43
|
+
to support analysis of Java and Ruby repositories, but it can easily be extended.
|
44
|
+
email:
|
45
|
+
- beatmadsen@gmail.com
|
46
|
+
executables:
|
47
|
+
- churn_vs_complexity
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- ".devcontainer/devcontainer.json"
|
52
|
+
- ".rubocop.yml"
|
53
|
+
- ".travis.yml"
|
54
|
+
- CHANGELOG.md
|
55
|
+
- Dockerfile
|
56
|
+
- Dockerfile.dev
|
57
|
+
- LICENSE.txt
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- bin/churn_vs_complexity
|
61
|
+
- lib/churn_vs_complexity.rb
|
62
|
+
- lib/churn_vs_complexity/churn.rb
|
63
|
+
- lib/churn_vs_complexity/cli.rb
|
64
|
+
- lib/churn_vs_complexity/complexity.rb
|
65
|
+
- lib/churn_vs_complexity/complexity/flog_calculator.rb
|
66
|
+
- lib/churn_vs_complexity/complexity/pmd_calculator.rb
|
67
|
+
- lib/churn_vs_complexity/concurrent_calculator.rb
|
68
|
+
- lib/churn_vs_complexity/config.rb
|
69
|
+
- lib/churn_vs_complexity/engine.rb
|
70
|
+
- lib/churn_vs_complexity/file_selector.rb
|
71
|
+
- lib/churn_vs_complexity/serializer.rb
|
72
|
+
- lib/churn_vs_complexity/version.rb
|
73
|
+
- tmp/pmd-support/ruleset.xml
|
74
|
+
- tmp/template/graph.html
|
75
|
+
- tmp/test-support/java/small-example/src/main/java/org/example/Main.java
|
76
|
+
- tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java
|
77
|
+
- tmp/test-support/txt/abc.txt
|
78
|
+
- tmp/test-support/txt/d.txt
|
79
|
+
- tmp/test-support/txt/ef.txt
|
80
|
+
- tmp/test-support/txt/ghij.txt
|
81
|
+
- tmp/test-support/txt/klm.txt
|
82
|
+
- tmp/test-support/txt/nopq.txt
|
83
|
+
- tmp/test-support/txt/r.txt
|
84
|
+
- tmp/test-support/txt/st.txt
|
85
|
+
- tmp/test-support/txt/uvx.txt
|
86
|
+
- tmp/test-support/txt/yz.txt
|
87
|
+
homepage: https://github.com/beatmadsen/churn_vs_complexity
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata:
|
91
|
+
source_code_uri: https://github.com/beatmadsen/churn_vs_complexity
|
92
|
+
changelog_uri: https://github.com/beatmadsen/churn_vs_complexity/CHANGELOG.md
|
93
|
+
rubygems_mfa_required: 'true'
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.3'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.5.11
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: A tool to visualise code complexity in projects and help direct refactoring
|
113
|
+
efforts.
|
114
|
+
test_files: []
|