zubat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f4eb5790313a7a672cad41fcf05c8c5515659f265a60952efbf3bb28fad7fb1
4
+ data.tar.gz: 0b94a1a09d207f79c9a32414655c4ec9706f47323659fc64d7f433f60ded012b
5
+ SHA512:
6
+ metadata.gz: 52c4ba88c4e15d3e947c0022b15f3da7477f7e8d5d7fbda9a0b43d7e8e61e13da446c59b7b6e54f425e32c31bf008bdbe1e3f19021f25c3c9ce728630e4eb586
7
+ data.tar.gz: 34b2ad121d67f463e93f1cb19153dba16764e33b59e73aef0a7e7704610b695d6a5320fd8a19bb4108787973aa19cc4ad1a3a2b56aa6e4e76835df034de8138c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ require:
2
+ - rubocop-rake
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.2
7
+
8
+ Layout/LineLength:
9
+ Max: 120
10
+
11
+ Style/Documentation:
12
+ Enabled: false
13
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 mizokami
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,51 @@
1
+ # zubat🦇
2
+
3
+ zubat is a gem that wraps around static analysis gems [Reek](https://github.com/troessner/reek) and [Flog](https://github.com/seattlerb/flog) to visualize trends' report of your Ruby code quality.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add zubat
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install zubat
14
+
15
+ ## Usage
16
+
17
+ ```sh
18
+ $ zubat path/to/awesome.rb
19
+ ```
20
+
21
+ There are a few options
22
+
23
+ ```sh
24
+ Usage: zubat [OPTIONS] FILE [FILE]...
25
+ -s, --silent
26
+ --root=ROOT
27
+ ```
28
+
29
+ ## Screenshot
30
+
31
+ ```sh
32
+ # For example, runs the following command under the ruby project root. It reports the result as shown in the screenshot below.
33
+
34
+ $ zubat lib/find.rb lib/csv.rb lib/uri.rb lib/set.rb
35
+ ```
36
+
37
+ ![](./screenshot.png)
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mizoR/zubat.
48
+
49
+ ## License
50
+
51
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/exe/zubat ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'zubat/cli'
5
+
6
+ Zubat::CLI.new.start(ARGV)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zubat
4
+ class Analizer
5
+ AnalizedResult = Data.define(:label, :stat)
6
+
7
+ # @param stat [Hash<String, Hash<String, Integer>>]
8
+ Stat = Data.define(:stat) do
9
+ # @return [Float]
10
+ def complexity_average(file)
11
+ stat.fetch(file, {}).fetch(:average, nil)
12
+ end
13
+
14
+ # @return [Float]
15
+ def complexity_total(file)
16
+ stat.fetch(file, {}).fetch(:total_score, nil)
17
+ end
18
+
19
+ def smell_scores(file)
20
+ stat.fetch(file, {}).fetch(:smells, {})
21
+ end
22
+
23
+ def smell_score(file, label)
24
+ stat.fetch(file).fetch(:smells).fetch(label, 0)
25
+ end
26
+
27
+ def each(*, **, &block)
28
+ stat.each(*, **, &block)
29
+ end
30
+ end
31
+
32
+ def analize(files:, commit:)
33
+ stat = {}
34
+
35
+ files.each do |file|
36
+ next unless commit.exists?(file:)
37
+
38
+ code = commit.show(file:)
39
+
40
+ next unless RubyCode.new(code:).valid?
41
+
42
+ stat[file] = FlogWrapper.new.examine(code).then do |examined|
43
+ { average: examined.average, total_score: examined.total_score }
44
+ end
45
+
46
+ stat[file][:smells] = ReekWrapper.new.examine(code).map(&:smell_type).tally
47
+ end
48
+
49
+ # return if smells.empty? && stat.empty?
50
+
51
+ stat = Stat.new(stat:)
52
+
53
+ AnalizedResult.new(label: "#{commit.time.iso8601} (#{commit.sha})", stat:)
54
+ end
55
+ end
56
+ end
data/lib/zubat/cli.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zubat'
4
+ require 'optparse'
5
+
6
+ module Zubat
7
+ class CLI
8
+ Argv = Data.define(:files, :silent, :root) do
9
+ def self.parse!(argv)
10
+ opt = OptionParser.new
11
+
12
+ root = nil
13
+ silent = false
14
+
15
+ opt.on('--silent', '-s') { silent = true }
16
+ opt.on('--root=ROOT') { |v| root = v }
17
+
18
+ files = opt.parse!(argv)
19
+
20
+ abort 'no files to process, aborting.' if files.empty? && argv.empty?
21
+
22
+ Dir.chdir(root || Dir.pwd) do
23
+ files = files.uniq
24
+ end
25
+
26
+ new(files:, silent:, root:)
27
+ end
28
+ end
29
+
30
+ class Progress
31
+ include Enumerable
32
+
33
+ def initialize(enum, silent:)
34
+ @enum = enum
35
+ @silent = silent
36
+ end
37
+
38
+ def each(&block)
39
+ @enum.each_with_index do |elem, i|
40
+ $stderr.print "\r#{%w[| / - \\][i % 4]} Analyzing... #{100 * i / @enum.size}%" unless @silent
41
+
42
+ block.call(elem)
43
+ end
44
+
45
+ warn "\r✨ Analized \n\n" unless @silent
46
+
47
+ @enum
48
+ end
49
+ end
50
+
51
+ def start(argv)
52
+ argv = Argv.parse!(argv)
53
+
54
+ generator = Generator.new
55
+
56
+ results = Dir.chdir(argv.root || Dir.pwd) do
57
+ files = argv.files
58
+
59
+ analizer = Zubat::Analizer.new
60
+
61
+ commits = Zubat::Commit.find(files:)
62
+
63
+ progress = Progress.new(commits, silent: argv.silent)
64
+
65
+ progress.map { |commit| analizer.analize(files:, commit:) }
66
+ end
67
+
68
+ file = generator.generate(results:)
69
+
70
+ puts "Generated - #{file}\n"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zubat
4
+ class Commit
5
+ attr_reader :time, :sha
6
+
7
+ def self.find(files:)
8
+ logs = GitCommandWrapper.new.log(files:)
9
+
10
+ commits = logs.map { |log| Zubat::Commit.new(sha: log.sha, time: log.time) }
11
+
12
+ commits.reverse!
13
+
14
+ commits
15
+ end
16
+
17
+ def initialize(sha:, time:)
18
+ @sha = sha
19
+ @time = time
20
+ end
21
+
22
+ def exists?(file:)
23
+ GitCommandWrapper.new.exists?(sha: @sha, file:)
24
+ end
25
+
26
+ def show(file:)
27
+ GitCommandWrapper.new.show(sha: @sha, file:)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'tempfile'
5
+ require 'flog'
6
+
7
+ module Zubat
8
+ class FlogWrapper
9
+ Flog = Data.define(:average, :total_score)
10
+
11
+ def examine(code)
12
+ flog = ::Flog.new
13
+
14
+ Tempfile.open do |file|
15
+ file.print(code)
16
+
17
+ file.close
18
+
19
+ flog.flog(file.path)
20
+
21
+ Flog.new(average: flog.average, total_score: flog.total_score)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zubat
4
+ class Generator
5
+ FILE = 'tmp/zubat/index.html'
6
+
7
+ def generate(results:)
8
+ erb = Zubat.root.join('templates/chart.html.erb').read
9
+
10
+ ylabels = []
11
+
12
+ results.each do |result|
13
+ result.stat.each do |ylabel, _|
14
+ ylabels << ylabel unless ylabels.include?(ylabel)
15
+ end
16
+ end
17
+
18
+ ylabels.sort!
19
+
20
+ datasets = []
21
+
22
+ results.each do |result|
23
+ xlabel = result.label
24
+
25
+ ylabels.each do |ylabel|
26
+ dataset = datasets.find { |ds| ds[:label] == ylabel }
27
+
28
+ unless dataset
29
+ dataset = { label: ylabel, data: [] }
30
+
31
+ datasets << dataset
32
+ end
33
+
34
+ dataset[:data] << {
35
+ label: xlabel,
36
+ complexity_total: result.stat.complexity_total(ylabel),
37
+ complexity_average: result.stat.complexity_average(ylabel),
38
+ smells_scores: result.stat.smell_scores(ylabel).map { |type, value| { type:, value: } }
39
+ }
40
+ end
41
+ end
42
+
43
+ html = ERB.new(erb).result_with_hash(datasets:)
44
+
45
+ file = File.expand_path(FILE)
46
+
47
+ FileUtils.mkdir_p(File.dirname(file))
48
+
49
+ IO.write(file, html)
50
+
51
+ file
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require 'securerandom'
6
+
7
+ module Zubat
8
+ class GitCommandWrapper
9
+ module Stub
10
+ def log(files:)
11
+ logs = files.map do
12
+ Log.new(sha: SecureRandom.hex(3), time: Time.at(rand(1_900_000_000..1_900_999_999)))
13
+ end
14
+
15
+ logs.sort_by!(&:time)
16
+
17
+ logs.reverse!
18
+
19
+ logs
20
+ end
21
+
22
+ def exists?(sha:, file:) # rubocop:disable Lint/UnusedMethodArgument
23
+ true
24
+ end
25
+
26
+ def show(sha:, file:) # rubocop:disable Lint/UnusedMethodArgument
27
+ <<~SCRIPT
28
+ class HelloWorld
29
+ def show
30
+ puts "Hello World"
31
+ end
32
+ end
33
+ SCRIPT
34
+ end
35
+ end
36
+
37
+ Log = Data.define(:sha, :time)
38
+
39
+ def self.new
40
+ instance = super
41
+ instance.extend(Stub) if Zubat.stubbed?
42
+ instance
43
+ end
44
+
45
+ def log(files:)
46
+ logs = `git log --oneline --pretty=format:'{ "sha": "%h", "time": "%ad" }' -- #{files.join(' ')}`.split("\n")
47
+
48
+ logs.map! do |log|
49
+ args = JSON.parse(log, symbolize_names: true)
50
+
51
+ Log.new(sha: args[:sha], time: Time.parse(args[:time]))
52
+ end
53
+
54
+ logs.sort_by!(&:time)
55
+
56
+ logs.reverse!
57
+
58
+ logs
59
+ end
60
+
61
+ def exists?(sha:, file:)
62
+ !`git ls-tree -r #{sha} -- #{file} 2>/dev/null`.empty?
63
+ end
64
+
65
+ def show(sha:, file:)
66
+ `git show #{sha}:#{file}`
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require 'time'
6
+ require 'reek'
7
+
8
+ module Zubat
9
+ class ReekWrapper
10
+ Smell = Data.define(:smell_type)
11
+
12
+ def examine(code)
13
+ smells = Reek::Examiner.new(String.new(code)).smells
14
+
15
+ smells.map { |smell| Smell.new(smell_type: smell.smell_type) }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zubat
4
+ class RubyCode
5
+ def initialize(code:)
6
+ @code = code
7
+ end
8
+
9
+ def valid?
10
+ RubyVM::InstructionSequence.compile(@code)
11
+
12
+ true
13
+ rescue SyntaxError
14
+ false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zubat
4
+ VERSION = '0.0.1'
5
+ end
data/lib/zubat.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'zubat/analizer'
4
+ require_relative 'zubat/commit'
5
+ require_relative 'zubat/flog_wrapper'
6
+ require_relative 'zubat/generator'
7
+ require_relative 'zubat/git_command_wrapper'
8
+ require_relative 'zubat/reek_wrapper'
9
+ require_relative 'zubat/ruby_code'
10
+ require_relative 'zubat/version'
11
+
12
+ module Zubat
13
+ class Error < StandardError; end
14
+
15
+ def self.stubbed=(stubbed)
16
+ @stubbed = stubbed
17
+ end
18
+
19
+ def self.stubbed?
20
+ !!@stubbed
21
+ end
22
+
23
+ def self.root
24
+ Pathname.new("#{__dir__}/..")
25
+ end
26
+ end
data/screenshot.png ADDED
Binary file
data/sig/zubat.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Zubat
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,430 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta name="robots" content="noindex,nofollow">
5
+ <meta charset="UTF-8">
6
+
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
8
+
9
+ <style>
10
+ body {
11
+ color: #777;
12
+ }
13
+
14
+ .pure-img-responsive {
15
+ max-width: 100%;
16
+ height: auto;
17
+ }
18
+
19
+ /*
20
+ Add transition to containers so they can push in and out.
21
+ */
22
+ #layout,
23
+ #menu,
24
+ .menu-link {
25
+ -webkit-transition: all 0.2s ease-out;
26
+ -moz-transition: all 0.2s ease-out;
27
+ -ms-transition: all 0.2s ease-out;
28
+ -o-transition: all 0.2s ease-out;
29
+ transition: all 0.2s ease-out;
30
+ }
31
+
32
+ /*
33
+ This is the parent `<div>` that contains the menu and the content area.
34
+ */
35
+ #layout {
36
+ position: relative;
37
+ left: 0;
38
+ padding-left: 0;
39
+ }
40
+ #layout.active #menu {
41
+ left: 200px;
42
+ width: 200px;
43
+ }
44
+
45
+ #layout.active .menu-link {
46
+ left: 200px;
47
+ }
48
+ /*
49
+ The content `<div>` is where all your content goes.
50
+ */
51
+ .content {
52
+ margin: 0 auto;
53
+ padding: 0 2em;
54
+ max-width: 1200px;
55
+ margin-bottom: 50px;
56
+ line-height: 1.6em;
57
+ }
58
+
59
+ .header {
60
+ margin: 0;
61
+ color: #333;
62
+ text-align: center;
63
+ padding: 2.5em 2em 0;
64
+ border-bottom: 1px solid #eee;
65
+ }
66
+ .header h1 {
67
+ margin: 0.2em 0;
68
+ font-size: 3em;
69
+ font-weight: 300;
70
+ }
71
+ .header h2 {
72
+ font-weight: 300;
73
+ color: #ccc;
74
+ padding: 0;
75
+ margin-top: 0;
76
+ }
77
+
78
+ .content-subhead {
79
+ margin: 50px 0 20px 0;
80
+ font-weight: 300;
81
+ color: #888;
82
+ }
83
+
84
+ /*
85
+ The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
86
+ appears on the left side of the page.
87
+ */
88
+
89
+ #menu {
90
+ margin-left: -200px; /* "#menu" width */
91
+ width: 200px;
92
+ position: fixed;
93
+ top: 0;
94
+ left: 0;
95
+ bottom: 0;
96
+ z-index: 1000; /* so the menu or its navicon stays above all content */
97
+ background: #191818;
98
+ overflow-y: auto;
99
+ }
100
+ /*
101
+ All anchors inside the menu should be styled like this.
102
+ */
103
+ #menu a {
104
+ color: #999;
105
+ border: none;
106
+ padding: 0.6em 0 0.6em 0.6em;
107
+ }
108
+
109
+ /*
110
+ Remove all background/borders, since we are applying them to #menu.
111
+ */
112
+ #menu .pure-menu,
113
+ #menu .pure-menu ul {
114
+ border: none;
115
+ background: transparent;
116
+ }
117
+
118
+ /*
119
+ Add that light border to separate items into groups.
120
+ */
121
+ #menu .pure-menu ul,
122
+ #menu .pure-menu .menu-item-divided {
123
+ border-top: 1px solid #333;
124
+ }
125
+ /*
126
+ Change color of the anchor links on hover/focus.
127
+ */
128
+ #menu .pure-menu li a:hover,
129
+ #menu .pure-menu li a:focus {
130
+ background: #333;
131
+ }
132
+
133
+ /*
134
+ This styles the selected menu item `<li>`.
135
+ */
136
+ #menu .pure-menu-selected,
137
+ #menu .pure-menu-heading {
138
+ background: #1f8dd6;
139
+ }
140
+ /*
141
+ This styles a link within a selected menu item `<li>`.
142
+ */
143
+ #menu .pure-menu-selected a {
144
+ color: #fff;
145
+ }
146
+
147
+ /*
148
+ This styles the menu heading.
149
+ */
150
+ #menu .pure-menu-heading {
151
+ font-size: 110%;
152
+ color: #fff;
153
+ margin: 0;
154
+ }
155
+
156
+ /* -- Dynamic Button For Responsive Menu -------------------------------------*/
157
+
158
+ /*
159
+ The button to open/close the Menu is custom-made and not part of Pure. Here's
160
+ how it works:
161
+ */
162
+
163
+ /*
164
+ `.menu-link` represents the responsive menu toggle that shows/hides on
165
+ small screens.
166
+ */
167
+ .menu-link {
168
+ position: fixed;
169
+ display: block; /* show this only on small screens */
170
+ top: 0;
171
+ left: 0; /* "#menu width" */
172
+ background: #000;
173
+ background: rgba(0,0,0,0.7);
174
+ font-size: 10px; /* change this value to increase/decrease button size */
175
+ z-index: 10;
176
+ width: 2em;
177
+ height: auto;
178
+ padding: 2.1em 1.6em;
179
+ }
180
+
181
+ .menu-link:hover,
182
+ .menu-link:focus {
183
+ background: #000;
184
+ }
185
+
186
+ .menu-link span {
187
+ position: relative;
188
+ display: block;
189
+ }
190
+
191
+ .menu-link span,
192
+ .menu-link span:before,
193
+ .menu-link span:after {
194
+ background-color: #fff;
195
+ pointer-events: none;
196
+ width: 100%;
197
+ height: 0.2em;
198
+ }
199
+
200
+ .menu-link span:before,
201
+ .menu-link span:after {
202
+ position: absolute;
203
+ margin-top: -0.6em;
204
+ content: " ";
205
+ }
206
+
207
+ .menu-link span:after {
208
+ margin-top: 0.6em;
209
+ }
210
+
211
+
212
+ /* -- Responsive Styles (Media Queries) ------------------------------------- */
213
+
214
+ /*
215
+ Hides the menu at `48em`, but modify this based on your app's needs.
216
+ */
217
+ @media (min-width: 48em) {
218
+
219
+ .header,
220
+ .content {
221
+ padding-left: 2em;
222
+ padding-right: 2em;
223
+ }
224
+
225
+ #layout {
226
+ padding-left: 200px; /* left col width "#menu" */
227
+ left: 0;
228
+ }
229
+ #menu {
230
+ left: 200px;
231
+ }
232
+
233
+ .menu-link {
234
+ position: fixed;
235
+ left: 200px;
236
+ display: none;
237
+ }
238
+
239
+ #layout.active .menu-link {
240
+ left: 200px;
241
+ }
242
+ }
243
+
244
+ @media (max-width: 48em) {
245
+ /* Only apply this when the window is small. Otherwise, the following
246
+ case results in extra padding on the left:
247
+ * Make the window small.
248
+ * Tap the menu to trigger the active state.
249
+ * Make the window large again.
250
+ */
251
+ #layout.active {
252
+ position: relative;
253
+ left: 200px;
254
+ }
255
+ }
256
+ </style>
257
+ <title>zubat</title>
258
+ </head>
259
+
260
+ <body>
261
+ <div id="layout">
262
+ <!-- Menu toggle -->
263
+ <a href="#menu" id="menuLink" class="menu-link">
264
+ <!-- Hamburger icon -->
265
+ <span></span>
266
+ </a>
267
+
268
+ <div id="menu">
269
+ <div class="pure-menu">
270
+ <a class="pure-menu-heading" href="#main">zubat</a>
271
+
272
+ <ul class="pure-menu-list">
273
+ <li class="pure-menu-item"><a href="#complexity_total" class="pure-menu-link">Complexity Total</a></li>
274
+ <li class="pure-menu-item"><a href="#complexity_average" class="pure-menu-link">Code Average</a></li>
275
+ <li class="pure-menu-item"><a href="#code_smells" class="pure-menu-link">Code smells</a></li>
276
+ </ul>
277
+ </div>
278
+ </div>
279
+
280
+ <div id="main">
281
+ <div class="header">
282
+ <h1>zubat</h1>
283
+ <h2>Visualize trends of your code complexity and smells</h2>
284
+ </div>
285
+
286
+ <div class="content">
287
+ <h2 id="complexity_total" class="content-subhead">Complexity Total</h2>
288
+ <p>
289
+ <canvas id="chart_of_code_complexity_total_trend" width="400" height="300"></canvas>
290
+ </p>
291
+
292
+ <h2 id="complexity_average" class="content-subhead">Complexity Average <small>- Complexity score per methods</small></h2>
293
+ <p>
294
+ <canvas id="chart_of_code_complexity_average_trend" width="400" height="300"></canvas>
295
+ </p>
296
+
297
+ <h2 id="code_smells" class="content-subhead">Code smells</h2>
298
+ <p>
299
+ <div id="select_of_code_smell_scores_trend"></div>
300
+ </p>
301
+ <p>
302
+ <canvas id="chart_of_code_smell_scores_trend" width="400" height="300"></canvas>
303
+ </p>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
309
+
310
+ <script>
311
+ var datasets = <%= datasets.to_json %>;
312
+
313
+ var ctx0 = document.getElementById("chart_of_code_complexity_total_trend");
314
+
315
+ new Chart(ctx0, {
316
+ type: 'line',
317
+ data: {
318
+ datasets: datasets
319
+ },
320
+ options: {
321
+ parsing: {
322
+ xAxisKey: 'label',
323
+ yAxisKey: 'complexity_total'
324
+ },
325
+ fill: true,
326
+ indexAxis: 'x',
327
+ scales: {
328
+ y: {
329
+ beginAtZero: true,
330
+ stacked: false
331
+ }
332
+ }
333
+ }
334
+ });
335
+
336
+ var ctx1 = document.getElementById("chart_of_code_complexity_average_trend");
337
+
338
+ new Chart(ctx1, {
339
+ type: 'line',
340
+ data: {
341
+ datasets: datasets
342
+ },
343
+ options: {
344
+ parsing: {
345
+ xAxisKey: 'label',
346
+ yAxisKey: 'complexity_average'
347
+ },
348
+ fill: true,
349
+ indexAxis: 'x',
350
+ scales: {
351
+ y: {
352
+ beginAtZero: true,
353
+ stacked: false
354
+ }
355
+ }
356
+ }
357
+ });
358
+
359
+ var ctx2 = document.getElementById("chart_of_code_smell_scores_trend");
360
+
361
+ var select = document.createElement('select');
362
+
363
+ datasets.map(item => {
364
+ let option = document.createElement('option');
365
+
366
+ option.text = item.label;
367
+ option.value = item.label;
368
+
369
+ select.add(option);
370
+ });
371
+
372
+
373
+
374
+ select.addEventListener('change', function () {
375
+ var file = select.value;
376
+
377
+ showCodeSmellChartFor(file);
378
+ });
379
+
380
+ document.getElementById("select_of_code_smell_scores_trend").appendChild(select);
381
+
382
+ var chart3 = null;
383
+
384
+ var showCodeSmellChartFor = function (file) {
385
+ var data = datasets.find(item => item.label === file).data;
386
+
387
+ var smellTypes = Array.from(new Set(data.flatMap(item => item.smells_scores).map(item => item.type))).sort();
388
+
389
+ var smellsets = smellTypes.map(smellType => {
390
+ return {
391
+ label: smellType,
392
+ data: data.map(item => {
393
+ var value = 0;
394
+ var smellScore = item.smells_scores.find(smellScore => smellScore.type === smellType);
395
+
396
+ if (smellScore) value = smellScore.value;
397
+
398
+ return { label: item.label, value: value };
399
+ })
400
+ };
401
+ });
402
+
403
+ if (chart3) chart3.destroy();
404
+
405
+ chart3 = new Chart(ctx2, {
406
+ type: 'line',
407
+ data: {
408
+ datasets: smellsets
409
+ },
410
+ options: {
411
+ parsing: {
412
+ xAxisKey: 'label',
413
+ yAxisKey: 'value'
414
+ },
415
+ fill: true,
416
+ indexAxis: 'x',
417
+ scales: {
418
+ y: {
419
+ beginAtZero: true,
420
+ stacked: true
421
+ }
422
+ }
423
+ }
424
+ });
425
+ };
426
+
427
+ showCodeSmellChartFor(datasets[0].label);
428
+ </script>
429
+ </body>
430
+ </html>
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zubat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - mizokami
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-11-08 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: reek
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ description: Visualize trends of your code complexity and smells
42
+ email:
43
+ - r.mizokami@gmail.com
44
+ executables:
45
+ - zubat
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - exe/zubat
55
+ - lib/zubat.rb
56
+ - lib/zubat/analizer.rb
57
+ - lib/zubat/cli.rb
58
+ - lib/zubat/commit.rb
59
+ - lib/zubat/flog_wrapper.rb
60
+ - lib/zubat/generator.rb
61
+ - lib/zubat/git_command_wrapper.rb
62
+ - lib/zubat/reek_wrapper.rb
63
+ - lib/zubat/ruby_code.rb
64
+ - lib/zubat/version.rb
65
+ - screenshot.png
66
+ - sig/zubat.rbs
67
+ - templates/chart.html.erb
68
+ homepage: https://github.com/mizoR/zubat
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/mizoR/zubat
73
+ source_code_uri: https://github.com/mizoR/zubat
74
+ changelog_uri: https://github.com/mizoR/zubat
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 3.2.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.4.10
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: zubat
94
+ test_files: []