cycromatic 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: 737d504d14a3c4223d39792e314668df88d39f8b
4
+ data.tar.gz: 628f94b6c7cf38e27ff1104fb083169a6e1f4cc6
5
+ SHA512:
6
+ metadata.gz: 8ee208f67d4f325f0cb5ee15d0820270f899700375471761ed818917696f3bef5df04999348b8ac75e8fed238def6b5788784ec1a7232a86edae8a1c433c799b
7
+ data.tar.gz: 9e8b3cbabf294d6620b218a67bd7aed7340ffafb69d41a364e6a66d8cc572f74655c120acd7d6a3c80c6c23a1cc3f2eb2331a45438796cb36512f9de49b43fef
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cycromatic.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cycromatic (0.1.0)
5
+ parser (~> 2.3)
6
+ rainbow (~> 2.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.3.0)
12
+ minitest (5.9.0)
13
+ parser (2.3.1.2)
14
+ ast (~> 2.2)
15
+ rainbow (2.1.0)
16
+ rake (10.5.0)
17
+
18
+ PLATFORMS
19
+ ruby
20
+
21
+ DEPENDENCIES
22
+ bundler (~> 1.12)
23
+ cycromatic!
24
+ minitest (~> 5.0)
25
+ rake (~> 10.0)
26
+
27
+ BUNDLED WITH
28
+ 1.12.5
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Cycromatic
2
+
3
+ Cycromatic calculates cyclomatic complexity of Ruby programs.
4
+
5
+ * Cyclomatic Complexity - https://en.wikipedia.org/wiki/Cyclomatic_complexity
6
+
7
+ ## Installation
8
+
9
+ Install by `gem`:
10
+
11
+ $ gem install cycromatic
12
+
13
+ ## Usage
14
+
15
+ Run `cycromatic` command to calculate complexity:
16
+
17
+ ```
18
+ $ cycromatic ruby_program.rb # Specify paths to .rb files
19
+ $ cycromatic app config # Specify directories including .rb files
20
+ ```
21
+
22
+ The output will be like the following:
23
+
24
+ ```
25
+ $ cycromatic ../contror/lib/contror/anf/translator.rb
26
+ ../contror/lib/contror/anf/translator.rb [toplevel]:1 1
27
+ ../contror/lib/contror/anf/translator.rb initialize:8 1
28
+ ../contror/lib/contror/anf/translator.rb translate:14 1
29
+ ../contror/lib/contror/anf/translator.rb with_new_block:20 3
30
+ ../contror/lib/contror/anf/translator.rb current_block:37 1
31
+ ../contror/lib/contror/anf/translator.rb push_stmt:41 1
32
+ ../contror/lib/contror/anf/translator.rb normalize_node:46 3
33
+ ../contror/lib/contror/anf/translator.rb translate0:59 52
34
+ ../contror/lib/contror/anf/translator.rb translate_arg:478 3
35
+ ../contror/lib/contror/anf/translator.rb translate_call:491 2
36
+ ../contror/lib/contror/anf/translator.rb translate_params:533 2
37
+ ../contror/lib/contror/anf/translator.rb value_node?:547 7
38
+ ../contror/lib/contror/anf/translator.rb fresh_var:566 1
39
+ ../contror/lib/contror/anf/translator.rb translate_var:571 5
40
+ ```
41
+
42
+ The tool accepts `--format=json` option to output in JSON format.
43
+
44
+ ## Calculation
45
+
46
+ It calculates complexities as the following:
47
+
48
+ * Basic block have complexity of 1 (base case)
49
+ * Branching construct have complexity of 1
50
+ * Loop construct have complexity of 1
51
+ * `&&` and `||` have complexity of 1
52
+ * Safe navigation operator have complexity of 1
53
+ * Passing iterator block does not introduce complexity
54
+
55
+ ## Contributing
56
+
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cycromatic.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cycromatic"
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
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,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cycromatic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cycromatic"
8
+ spec.version = Cycromatic::VERSION
9
+ spec.authors = ["Soutaro Matsumoto"]
10
+ spec.email = ["matsumoto@soutaro.com"]
11
+
12
+ spec.summary = %q{Cyclomatic Complexity}
13
+ spec.description = %q{Compute Cyclomatic Complexity of Ruby programs}
14
+ spec.homepage = "https://github.com/cycromatic"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.12"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.0"
24
+
25
+ spec.add_dependency "rainbow", "~> 2.1"
26
+ spec.add_dependency "parser", "~> 2.3"
27
+ end
data/exe/cycromatic ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+
5
+ require 'cycromatic'
6
+ require 'cycromatic/cli'
7
+
8
+ Cycromatic::CLI.start
@@ -0,0 +1,79 @@
1
+ module Cycromatic
2
+ class Calculator
3
+ attr_reader :node
4
+
5
+ def initialize(node:)
6
+ @node = node
7
+ end
8
+
9
+ def each_complexity(&block)
10
+ if block_given?
11
+ calculate_toplevel node, &block
12
+ else
13
+ enum_for :each_complexity
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ COMPLEX_NODE_TYPES = Set.new([:if, :while, :while_post, :when, :resbody, :csend, :for,
20
+ :and, :or,
21
+ :optarg, :kwoptarg])
22
+
23
+ def calculate_toplevel(node, &block)
24
+ value = calculate_node(node, &block)
25
+ yield Complexity.new(type: :toplevel, node: node, value: value + 1)
26
+ end
27
+
28
+ def calculate_def(node, &block)
29
+ value = 0
30
+
31
+ case node.type
32
+ when :def
33
+ args = node.children[1]
34
+ body = node.children[2]
35
+ when :defs
36
+ args = node.children[2]
37
+ body = node.children[3]
38
+ end
39
+
40
+ value += calculate_node(args, &block)
41
+ value += calculate_node(body, &block) if body
42
+
43
+ yield Complexity.new(type: :method, node: node, value: value + 1)
44
+ end
45
+
46
+ def calculate_node(node, &block)
47
+ count = 0
48
+
49
+ case node.type
50
+ when :def
51
+ calculate_def node, &block
52
+ return 0
53
+ when :defs
54
+ calculate_def node, &block
55
+ return calculate_node(node.children[0], &block)
56
+ when :case
57
+ if node.children.last
58
+ count = 1
59
+ end
60
+ when :rescue
61
+ if node.children.last
62
+ count = 1
63
+ end
64
+ else
65
+ if COMPLEX_NODE_TYPES.include?(node.type)
66
+ count = 1
67
+ end
68
+ end
69
+
70
+ count + node.children.flat_map do |child|
71
+ if child.is_a? Parser::AST::Node
72
+ [calculate_node(child, &block)]
73
+ else
74
+ []
75
+ end
76
+ end.inject(0) {|x, y| x + y }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,50 @@
1
+ require 'optparse'
2
+ require 'rainbow'
3
+ require 'pathname'
4
+
5
+ module Cycromatic
6
+ class CLI
7
+ attr_reader :args
8
+
9
+ def self.start(args = ARGV)
10
+ self.new(args).run
11
+ end
12
+
13
+ def initialize(args)
14
+ @args = args
15
+
16
+ OptionParser.new do |opts|
17
+ opts.on("--format FORMAT") {|fmt| @format = fmt }
18
+ end.parse!(args)
19
+ end
20
+
21
+ def run
22
+ formatter = @format == 'json' ? JSONFormatter.new(io: STDOUT) : TextFormatter.new(io: STDOUT)
23
+
24
+ FileEnumerator.new(paths: paths).each do |path|
25
+ formatter.started path: path
26
+ begin
27
+ node = Parser::CurrentRuby.parse(path.read, path.to_s)
28
+ if node
29
+ Calculator.new(node: node).each_complexity do |complexity|
30
+ formatter.calculated(path: path, complexity: complexity)
31
+ end
32
+ end
33
+ rescue => exn
34
+ require 'pp'
35
+ p exn
36
+ pp exn.backtrace
37
+ formatter.error(path: path, exception: exn)
38
+ ensure
39
+ formatter.finished path: path
40
+ end
41
+ end
42
+
43
+ formatter.completed
44
+ end
45
+
46
+ def paths
47
+ args.map {|arg| Pathname(arg) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module Cycromatic
2
+ class Complexity
3
+ attr_reader :type
4
+ attr_reader :node
5
+ attr_reader :value
6
+
7
+ def initialize(type:, node:, value:)
8
+ @type = type
9
+ @node = node
10
+ @value = value
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ module Cycromatic
2
+ class FileEnumerator
3
+ attr_reader :paths
4
+
5
+ def initialize(paths:)
6
+ @paths = paths
7
+ end
8
+
9
+ def each(&block)
10
+ if block_given?
11
+ paths.each do |path|
12
+ case
13
+ when path.file?
14
+ yield path
15
+ when path.directory?
16
+ enumerate_files_in_dir(path, &block)
17
+ end
18
+ end
19
+ else
20
+ self.enum_for :each
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def enumerate_files_in_dir(path, &block)
27
+ if path.basename.to_s =~ /\A\.[^\.]+/
28
+ # skip hidden paths
29
+ return
30
+ end
31
+
32
+ case
33
+ when path.directory?
34
+ path.children.each do |child|
35
+ enumerate_files_in_dir child, &block
36
+ end
37
+ when path.file?
38
+ if is_ruby_file?(path)
39
+ yield path
40
+ end
41
+ end
42
+ end
43
+
44
+ def is_ruby_file?(path)
45
+ case
46
+ when path.extname == ".rb"
47
+ true
48
+ when path.extname == ".gemspec"
49
+ true
50
+ when path.basename.to_s == "Rakefile"
51
+ true
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ module Cycromatic
2
+ class JSONFormatter
3
+ attr_reader :io
4
+
5
+ def initialize(io:)
6
+ @io = io
7
+ @results = {}
8
+ end
9
+
10
+ def started(path:)
11
+ @path_results = []
12
+ @results[path.to_s] = { results: @path_results }
13
+ end
14
+
15
+ def finished(path:)
16
+ @path_results = nil
17
+ end
18
+
19
+ def completed
20
+ @io.puts @results.to_json
21
+ end
22
+
23
+ def error(path:, exception:)
24
+ @results[path.to_s] = {
25
+ error: {
26
+ message: exception.to_s,
27
+ trace: exception.backtrace
28
+ }
29
+ }
30
+ end
31
+
32
+ def calculated(path:, complexity:)
33
+ loc = complexity.node.loc
34
+ name = case complexity.type
35
+ when :toplevel
36
+ "[toplevel]"
37
+ when :method
38
+ complexity.node.children.find {|c| c.is_a? Symbol }.to_s
39
+ end
40
+
41
+ @path_results << {
42
+ method: name,
43
+ line: [loc.first_line, loc.last_line],
44
+ complexity: complexity.value
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ module Cycromatic
2
+ class TextFormatter
3
+ attr_reader :io
4
+
5
+ def initialize(io:)
6
+ @io = io
7
+ end
8
+
9
+ def started(path:)
10
+ end
11
+
12
+ def finished(path:)
13
+ end
14
+
15
+ def completed
16
+ end
17
+
18
+ def error(path:, exception:)
19
+ io.puts "#{path}\t(error)"
20
+ end
21
+
22
+ def calculated(path:, complexity:)
23
+ loc = complexity.node.loc
24
+ name = case complexity.type
25
+ when :toplevel
26
+ "[toplevel]"
27
+ when :method
28
+ complexity.node.children.find {|c| c.is_a? Symbol }.to_s
29
+ end
30
+
31
+ io.puts "#{path}\t#{name}:#{loc.first_line}\t#{complexity.value}"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Cycromatic
2
+ VERSION = "0.1.0"
3
+ end
data/lib/cycromatic.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'parser/current'
2
+ require 'json'
3
+
4
+ require "cycromatic/version"
5
+ require 'cycromatic/complexity'
6
+ require 'cycromatic/calculator'
7
+ require 'cycromatic/file_enumerator'
8
+ require 'cycromatic/text_formatter'
9
+ require 'cycromatic/json_formatter'
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cycromatic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Soutaro Matsumoto
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rainbow
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: parser
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.3'
83
+ description: Compute Cyclomatic Complexity of Ruby programs
84
+ email:
85
+ - matsumoto@soutaro.com
86
+ executables:
87
+ - cycromatic
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - README.md
96
+ - Rakefile
97
+ - bin/console
98
+ - bin/setup
99
+ - cycromatic.gemspec
100
+ - exe/cycromatic
101
+ - lib/cycromatic.rb
102
+ - lib/cycromatic/calculator.rb
103
+ - lib/cycromatic/cli.rb
104
+ - lib/cycromatic/complexity.rb
105
+ - lib/cycromatic/file_enumerator.rb
106
+ - lib/cycromatic/json_formatter.rb
107
+ - lib/cycromatic/text_formatter.rb
108
+ - lib/cycromatic/version.rb
109
+ homepage: https://github.com/cycromatic
110
+ licenses: []
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.5.1
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Cyclomatic Complexity
132
+ test_files: []