polariscope 0.1.0

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: '012182ad9b21b397a1b59d91c199bef8a751baa727c0a3d95703dd0df13ee3d2'
4
+ data.tar.gz: fa12186f4461ba71e1a9d252c4115fd009f006e3abca2b0ab957bf9862140e89
5
+ SHA512:
6
+ metadata.gz: 8a25dd7a42b93ff2081b7042fbae6c3f65c769933ad1792f345913c84b9c7daafc735f2a92e0a8e1d5c6d79fe46f481b8e227dac3afcfc54034778ab04a7ac1f
7
+ data.tar.gz: 4d9acf916ef3a6ffdc51dd234e3f5dded843d726103320eeddea02a8da7b828803bae605f1fc3cd8aa6331daf58aa8faf6a2e62bfc36185b0487b39d88d01352
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ inherit_gem:
2
+ rubocop-infinum: rubocop.yml
3
+
4
+ require:
5
+ - rubocop-infinum
6
+ - rubocop-rake
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 3.0
10
+
11
+ RSpec/MultipleExpectations:
12
+ Max: 5
13
+
14
+ Style/FrozenStringLiteralComment:
15
+ Exclude:
16
+ - 'exe/polariscope'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-08-07
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in infinum_azure.gemspec
8
+ gemspec
9
+
10
+ gem 'bundler'
11
+ gem 'pry'
12
+ gem 'pry-byebug'
13
+ gem 'pry-rails'
14
+ gem 'rails', '~> 7.0'
15
+ gem 'rake', '~> 12.0'
16
+ gem 'rspec', '~> 3.0'
17
+ gem 'rubocop-infinum'
18
+ gem 'rubocop-rake'
data/Gemfile.lock ADDED
@@ -0,0 +1,285 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ polariscope (0.1.0)
5
+ bundler
6
+ bundler-audit
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actioncable (7.1.3.4)
12
+ actionpack (= 7.1.3.4)
13
+ activesupport (= 7.1.3.4)
14
+ nio4r (~> 2.0)
15
+ websocket-driver (>= 0.6.1)
16
+ zeitwerk (~> 2.6)
17
+ actionmailbox (7.1.3.4)
18
+ actionpack (= 7.1.3.4)
19
+ activejob (= 7.1.3.4)
20
+ activerecord (= 7.1.3.4)
21
+ activestorage (= 7.1.3.4)
22
+ activesupport (= 7.1.3.4)
23
+ mail (>= 2.7.1)
24
+ net-imap
25
+ net-pop
26
+ net-smtp
27
+ actionmailer (7.1.3.4)
28
+ actionpack (= 7.1.3.4)
29
+ actionview (= 7.1.3.4)
30
+ activejob (= 7.1.3.4)
31
+ activesupport (= 7.1.3.4)
32
+ mail (~> 2.5, >= 2.5.4)
33
+ net-imap
34
+ net-pop
35
+ net-smtp
36
+ rails-dom-testing (~> 2.2)
37
+ actionpack (7.1.3.4)
38
+ actionview (= 7.1.3.4)
39
+ activesupport (= 7.1.3.4)
40
+ nokogiri (>= 1.8.5)
41
+ racc
42
+ rack (>= 2.2.4)
43
+ rack-session (>= 1.0.1)
44
+ rack-test (>= 0.6.3)
45
+ rails-dom-testing (~> 2.2)
46
+ rails-html-sanitizer (~> 1.6)
47
+ actiontext (7.1.3.4)
48
+ actionpack (= 7.1.3.4)
49
+ activerecord (= 7.1.3.4)
50
+ activestorage (= 7.1.3.4)
51
+ activesupport (= 7.1.3.4)
52
+ globalid (>= 0.6.0)
53
+ nokogiri (>= 1.8.5)
54
+ actionview (7.1.3.4)
55
+ activesupport (= 7.1.3.4)
56
+ builder (~> 3.1)
57
+ erubi (~> 1.11)
58
+ rails-dom-testing (~> 2.2)
59
+ rails-html-sanitizer (~> 1.6)
60
+ activejob (7.1.3.4)
61
+ activesupport (= 7.1.3.4)
62
+ globalid (>= 0.3.6)
63
+ activemodel (7.1.3.4)
64
+ activesupport (= 7.1.3.4)
65
+ activerecord (7.1.3.4)
66
+ activemodel (= 7.1.3.4)
67
+ activesupport (= 7.1.3.4)
68
+ timeout (>= 0.4.0)
69
+ activestorage (7.1.3.4)
70
+ actionpack (= 7.1.3.4)
71
+ activejob (= 7.1.3.4)
72
+ activerecord (= 7.1.3.4)
73
+ activesupport (= 7.1.3.4)
74
+ marcel (~> 1.0)
75
+ activesupport (7.1.3.4)
76
+ base64
77
+ bigdecimal
78
+ concurrent-ruby (~> 1.0, >= 1.0.2)
79
+ connection_pool (>= 2.2.5)
80
+ drb
81
+ i18n (>= 1.6, < 2)
82
+ minitest (>= 5.1)
83
+ mutex_m
84
+ tzinfo (~> 2.0)
85
+ ast (2.4.2)
86
+ base64 (0.2.0)
87
+ bigdecimal (3.1.8)
88
+ builder (3.3.0)
89
+ bundler-audit (0.9.1)
90
+ bundler (>= 1.2.0, < 3)
91
+ thor (~> 1.0)
92
+ byebug (11.1.3)
93
+ coderay (1.1.3)
94
+ concurrent-ruby (1.3.3)
95
+ connection_pool (2.4.1)
96
+ crass (1.0.6)
97
+ date (3.3.4)
98
+ diff-lcs (1.5.1)
99
+ drb (2.2.1)
100
+ erubi (1.13.0)
101
+ globalid (1.2.1)
102
+ activesupport (>= 6.1)
103
+ i18n (1.14.5)
104
+ concurrent-ruby (~> 1.0)
105
+ io-console (0.7.2)
106
+ irb (1.14.0)
107
+ rdoc (>= 4.0.0)
108
+ reline (>= 0.4.2)
109
+ json (2.7.2)
110
+ language_server-protocol (3.17.0.3)
111
+ loofah (2.22.0)
112
+ crass (~> 1.0.2)
113
+ nokogiri (>= 1.12.0)
114
+ mail (2.8.1)
115
+ mini_mime (>= 0.1.1)
116
+ net-imap
117
+ net-pop
118
+ net-smtp
119
+ marcel (1.0.4)
120
+ method_source (1.1.0)
121
+ mini_mime (1.1.5)
122
+ minitest (5.24.1)
123
+ mutex_m (0.2.0)
124
+ net-imap (0.4.14)
125
+ date
126
+ net-protocol
127
+ net-pop (0.1.2)
128
+ net-protocol
129
+ net-protocol (0.2.2)
130
+ timeout
131
+ net-smtp (0.5.0)
132
+ net-protocol
133
+ nio4r (2.7.3)
134
+ nokogiri (1.16.7-aarch64-linux)
135
+ racc (~> 1.4)
136
+ nokogiri (1.16.7-arm-linux)
137
+ racc (~> 1.4)
138
+ nokogiri (1.16.7-arm64-darwin)
139
+ racc (~> 1.4)
140
+ nokogiri (1.16.7-x86-linux)
141
+ racc (~> 1.4)
142
+ nokogiri (1.16.7-x86_64-darwin)
143
+ racc (~> 1.4)
144
+ nokogiri (1.16.7-x86_64-linux)
145
+ racc (~> 1.4)
146
+ parallel (1.25.1)
147
+ parser (3.3.4.0)
148
+ ast (~> 2.4.1)
149
+ racc
150
+ pry (0.14.2)
151
+ coderay (~> 1.1)
152
+ method_source (~> 1.0)
153
+ pry-byebug (3.10.1)
154
+ byebug (~> 11.0)
155
+ pry (>= 0.13, < 0.15)
156
+ pry-rails (0.3.11)
157
+ pry (>= 0.13.0)
158
+ psych (5.1.2)
159
+ stringio
160
+ racc (1.8.1)
161
+ rack (3.1.7)
162
+ rack-session (2.0.0)
163
+ rack (>= 3.0.0)
164
+ rack-test (2.1.0)
165
+ rack (>= 1.3)
166
+ rackup (2.1.0)
167
+ rack (>= 3)
168
+ webrick (~> 1.8)
169
+ rails (7.1.3.4)
170
+ actioncable (= 7.1.3.4)
171
+ actionmailbox (= 7.1.3.4)
172
+ actionmailer (= 7.1.3.4)
173
+ actionpack (= 7.1.3.4)
174
+ actiontext (= 7.1.3.4)
175
+ actionview (= 7.1.3.4)
176
+ activejob (= 7.1.3.4)
177
+ activemodel (= 7.1.3.4)
178
+ activerecord (= 7.1.3.4)
179
+ activestorage (= 7.1.3.4)
180
+ activesupport (= 7.1.3.4)
181
+ bundler (>= 1.15.0)
182
+ railties (= 7.1.3.4)
183
+ rails-dom-testing (2.2.0)
184
+ activesupport (>= 5.0.0)
185
+ minitest
186
+ nokogiri (>= 1.6)
187
+ rails-html-sanitizer (1.6.0)
188
+ loofah (~> 2.21)
189
+ nokogiri (~> 1.14)
190
+ railties (7.1.3.4)
191
+ actionpack (= 7.1.3.4)
192
+ activesupport (= 7.1.3.4)
193
+ irb
194
+ rackup (>= 1.0.0)
195
+ rake (>= 12.2)
196
+ thor (~> 1.0, >= 1.2.2)
197
+ zeitwerk (~> 2.6)
198
+ rainbow (3.1.1)
199
+ rake (12.3.3)
200
+ rdoc (6.7.0)
201
+ psych (>= 4.0.0)
202
+ regexp_parser (2.9.2)
203
+ reline (0.5.9)
204
+ io-console (~> 0.5)
205
+ rexml (3.3.4)
206
+ strscan
207
+ rspec (3.13.0)
208
+ rspec-core (~> 3.13.0)
209
+ rspec-expectations (~> 3.13.0)
210
+ rspec-mocks (~> 3.13.0)
211
+ rspec-core (3.13.0)
212
+ rspec-support (~> 3.13.0)
213
+ rspec-expectations (3.13.1)
214
+ diff-lcs (>= 1.2.0, < 2.0)
215
+ rspec-support (~> 3.13.0)
216
+ rspec-mocks (3.13.1)
217
+ diff-lcs (>= 1.2.0, < 2.0)
218
+ rspec-support (~> 3.13.0)
219
+ rspec-support (3.13.1)
220
+ rubocop (1.65.1)
221
+ json (~> 2.3)
222
+ language_server-protocol (>= 3.17.0)
223
+ parallel (~> 1.10)
224
+ parser (>= 3.3.0.2)
225
+ rainbow (>= 2.2.2, < 4.0)
226
+ regexp_parser (>= 2.4, < 3.0)
227
+ rexml (>= 3.2.5, < 4.0)
228
+ rubocop-ast (>= 1.31.1, < 2.0)
229
+ ruby-progressbar (~> 1.7)
230
+ unicode-display_width (>= 2.4.0, < 3.0)
231
+ rubocop-ast (1.32.0)
232
+ parser (>= 3.3.1.0)
233
+ rubocop-infinum (0.8.0)
234
+ rubocop (>= 1.28.0)
235
+ rubocop-performance
236
+ rubocop-rails
237
+ rubocop-rspec
238
+ rubocop-performance (1.21.1)
239
+ rubocop (>= 1.48.1, < 2.0)
240
+ rubocop-ast (>= 1.31.1, < 2.0)
241
+ rubocop-rails (2.25.1)
242
+ activesupport (>= 4.2.0)
243
+ rack (>= 1.1)
244
+ rubocop (>= 1.33.0, < 2.0)
245
+ rubocop-ast (>= 1.31.1, < 2.0)
246
+ rubocop-rake (0.6.0)
247
+ rubocop (~> 1.0)
248
+ rubocop-rspec (3.0.4)
249
+ rubocop (~> 1.61)
250
+ ruby-progressbar (1.13.0)
251
+ stringio (3.1.1)
252
+ strscan (3.1.0)
253
+ thor (1.3.1)
254
+ timeout (0.4.1)
255
+ tzinfo (2.0.6)
256
+ concurrent-ruby (~> 1.0)
257
+ unicode-display_width (2.5.0)
258
+ webrick (1.8.1)
259
+ websocket-driver (0.7.6)
260
+ websocket-extensions (>= 0.1.0)
261
+ websocket-extensions (0.1.5)
262
+ zeitwerk (2.6.17)
263
+
264
+ PLATFORMS
265
+ aarch64-linux
266
+ arm-linux
267
+ arm64-darwin
268
+ x86-linux
269
+ x86_64-darwin
270
+ x86_64-linux
271
+
272
+ DEPENDENCIES
273
+ bundler
274
+ polariscope!
275
+ pry
276
+ pry-byebug
277
+ pry-rails
278
+ rails (~> 7.0)
279
+ rake (~> 12.0)
280
+ rspec (~> 3.0)
281
+ rubocop-infinum
282
+ rubocop-rake
283
+
284
+ BUNDLED WITH
285
+ 2.5.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Marko Ćilimković
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,35 @@
1
+ # Polariscope
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/polariscope`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ 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.
26
+
27
+ 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).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/polariscope.
32
+
33
+ ## License
34
+
35
+ 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: [:spec, :rubocop]
data/exe/polariscope ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'polariscope'
4
+
5
+ if ARGV.empty?
6
+ puts 'Usage: polariscope scan'
7
+ exit
8
+ end
9
+
10
+ command = ARGV[0]
11
+
12
+ case command
13
+ when 'scan'
14
+ puts Polariscope.scan
15
+ else
16
+ puts "Unknown command: #{command}"
17
+ puts 'Usage: polariscope scan'
18
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ module Color
5
+ class Hsl
6
+ MAX_HUE = 120
7
+
8
+ class << self
9
+ def for(score)
10
+ new(score).hsl
11
+ end
12
+ end
13
+
14
+ def initialize(score)
15
+ @score = score
16
+ end
17
+
18
+ def hsl
19
+ return '' unless score
20
+
21
+ "hsl(#{hue}, 100%, 45%)"
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :score
27
+
28
+ def hue
29
+ (MAX_HUE * (rounded_score / 100.0)).round
30
+ end
31
+
32
+ def rounded_score
33
+ (score / 5).round * 5
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ class FileContent < String
5
+ class << self
6
+ def for(path)
7
+ file_path = File.join(Dir.pwd, path)
8
+
9
+ File.exist?(file_path) ? new(File.read(file_path)) : new
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require_relative 'gemfile_health_score'
5
+
6
+ module Polariscope
7
+ module Scanner
8
+ class CodebaseHealthScore
9
+ def initialize(gemfile_content:, gemfile_lock_content:, bundler_audit_config_content:)
10
+ @gemfile_content = gemfile_content
11
+ @gemfile_lock_content = gemfile_lock_content
12
+ @bundler_audit_config_content = bundler_audit_config_content
13
+ end
14
+
15
+ def health_score
16
+ return nil if blank?(gemfile_content) || blank?(gemfile_lock_content)
17
+
18
+ GemfileHealthScore.new(
19
+ gemfile_path: gemfile_file.path,
20
+ gemfile_lock_content: gemfile_lock_content,
21
+ bundler_audit_config_path: bundler_audit_config_file.path
22
+ ).health_score
23
+ ensure
24
+ gemfile_file.unlink
25
+ bundler_audit_config_file.unlink
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :gemfile_content
31
+ attr_reader :gemfile_lock_content
32
+ attr_reader :bundler_audit_config_content
33
+
34
+ def gemfile_file
35
+ @gemfile_file ||= begin
36
+ file = Tempfile.new('Gemfile')
37
+ file.write(gemfile_content.gsub("gemspec\n", ''))
38
+ file.close
39
+ file
40
+ end
41
+ end
42
+
43
+ def bundler_audit_config_file
44
+ @bundler_audit_config_file ||= begin
45
+ file = Tempfile.new('.bundler-audit.yml')
46
+ file.write(bundler_audit_config_content)
47
+ file.close
48
+ file
49
+ end
50
+ end
51
+
52
+ def blank?(value)
53
+ value.nil? || value == ''
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ module Scanner
5
+ class GemHealthScore
6
+ def initialize(all_versions:, current_version:, severities: [])
7
+ @all_versions = all_versions
8
+ @current_version = current_version
9
+ @severities = severities
10
+ end
11
+
12
+ def health_score
13
+ return 100 if up_to_date?
14
+
15
+ score = 100
16
+ score *= (1.0 + first_outdated_segment)**-Math.log(first_outdated_segment_severity)
17
+ score *= (1.0 + new_versions.count)**-Math.log(1.07)
18
+ score
19
+ end
20
+
21
+ def up_to_date?
22
+ current_version == latest_version
23
+ end
24
+
25
+ def first_outdated_segment_severity
26
+ return 1 if first_outdated_segment_index.nil?
27
+
28
+ severities[first_outdated_segment_index]
29
+ end
30
+
31
+ def first_outdated_segment_index
32
+ segments_delta.find_index(&:positive?)
33
+ end
34
+
35
+ def first_outdated_segment
36
+ segments_delta.find(&:positive?) || 0
37
+ end
38
+
39
+ def segments_delta
40
+ current_version.segments.grep(Integer).zip(latest_version.segments.grep(Integer))
41
+ .map { |current, latest| current && latest ? latest - current : 0 }
42
+ end
43
+
44
+ def major_version_penalty
45
+ major_outdated? ? 1 : 0
46
+ end
47
+
48
+ def major_outdated?
49
+ latest_version.segments[0] > current_version.segments[0]
50
+ end
51
+
52
+ def latest_version
53
+ @latest_version ||= new_versions.max || current_version
54
+ end
55
+
56
+ def new_versions
57
+ @new_versions ||= all_versions.select { |version| version > current_version }
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :all_versions
63
+ attr_reader :current_version
64
+ attr_reader :severities
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Polariscope
6
+ module Scanner
7
+ class GemVersions
8
+ def initialize(dependency_names, spec_type:)
9
+ @dependency_names = dependency_names.to_set
10
+ @spec_type = spec_type
11
+ @gem_versions = Hash.new { |h, k| h[k] = [] }
12
+
13
+ fetch_gems
14
+ end
15
+
16
+ def versions_for(gem_name)
17
+ @gem_versions[gem_name]
18
+ end
19
+
20
+ private
21
+
22
+ def fetch_gems
23
+ gem_tuples = Gem::SpecFetcher.fetcher.detect(@spec_type) do |name_tuple|
24
+ @dependency_names.include?(name_tuple.name)
25
+ end
26
+
27
+ gem_tuples.each { |gem_tuple| @gem_versions[gem_tuple.first.name] << gem_tuple.first.version }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'bundler/audit/configuration'
5
+ require 'bundler/audit/database'
6
+ require 'set'
7
+ require_relative 'gem_versions'
8
+ require_relative 'gem_health_score'
9
+
10
+ module Polariscope
11
+ module Scanner
12
+ class GemfileHealthScore # rubocop:disable Metrics/ClassLength
13
+ GEM_PRIORITIES = { rails: 10.0 }.freeze
14
+ DEFAULT_PRIORITY = 1.0
15
+ GROUP_PRIORITIES = { default: 2.0, production: 2.0 }.freeze
16
+ SEVERITIES = [1.7, 1.15, 1.01, 1.005].freeze
17
+ FALLBACK_ADVISORY_PENALTY = 0.5
18
+ ADVISORY_PENALTY_MAP = {
19
+ none: 0.0,
20
+ low: 0.5,
21
+ medium: 1.0,
22
+ high: 3.0,
23
+ critical: 5.0
24
+ }.freeze
25
+
26
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
27
+ gemfile_path:, gemfile_lock_content:, gem_priorities: GEM_PRIORITIES, default_priority: DEFAULT_PRIORITY,
28
+ group_priorities: GROUP_PRIORITIES, severities: SEVERITIES, spec_type: :released,
29
+ advisory_penalty_map: ADVISORY_PENALTY_MAP, fallback_advisory_penalty: FALLBACK_ADVISORY_PENALTY,
30
+ update_audit_database: false, bundler_audit_config_path: ''
31
+ )
32
+ @lockfile_parser = Bundler::LockfileParser.new(gemfile_lock_content)
33
+ @gemfile_path = gemfile_path
34
+ @dependencies = installed_dependencies
35
+ @gem_priorities = gem_priorities
36
+ @default_priority = default_priority
37
+ @group_priorities = group_priorities
38
+ @severities = severities
39
+ @spec_type = spec_type
40
+ @advisory_penalty_map = advisory_penalty_map
41
+ @fallback_advisory_penalty = fallback_advisory_penalty
42
+ @bundler_audit_config_path = bundler_audit_config_path
43
+
44
+ update_audit_database! if update_audit_database
45
+ end
46
+
47
+ def health_score
48
+ return nil if dependencies.empty?
49
+
50
+ ((1.0 - major_version_penalty_score) * weighted_gem_health_score * advisories_score).round(2)
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :dependencies
56
+ attr_reader :lockfile_parser
57
+ attr_reader :advisory_penalty_map
58
+ attr_reader :fallback_advisory_penalty
59
+ attr_reader :bundler_audit_config_path
60
+
61
+ def major_version_penalties
62
+ dependencies.map do |dependency|
63
+ current_version, all_versions = dependency_versions(dependency)
64
+
65
+ GemHealthScore.new(all_versions: all_versions, current_version: current_version).major_version_penalty
66
+ end
67
+ end
68
+
69
+ def major_version_penalty_score
70
+ dependency_priorities.zip(major_version_penalties).sum { |a| a.inject(:*) } / dependency_priorities.sum
71
+ end
72
+
73
+ def weighted_gem_health_score
74
+ dependency_priorities.zip(dependency_health_scores).sum { |a| a.inject(:*) } / dependency_priorities.sum
75
+ end
76
+
77
+ def dependency_health_scores
78
+ dependencies.map do |dependency|
79
+ current_version, all_versions = dependency_versions(dependency)
80
+
81
+ GemHealthScore.new(
82
+ all_versions: all_versions,
83
+ current_version: current_version,
84
+ severities: @severities
85
+ ).health_score
86
+ end
87
+ end
88
+
89
+ def dependency_priorities
90
+ @dependency_priorities ||= dependencies.map { |dependency| dependency_priority(dependency) }
91
+ end
92
+
93
+ def dependency_priority(dependency)
94
+ @gem_priorities[dependency.name.to_sym] || @group_priorities[dependency.groups.first] || @default_priority
95
+ end
96
+
97
+ def dependency_versions(dependency)
98
+ [current_dependency_version(dependency), gem_versions.versions_for(dependency.name)]
99
+ end
100
+
101
+ def current_dependency_version(dependency)
102
+ lockfile_parser.specs.find { |spec| dependency.name == spec.name }.version
103
+ end
104
+
105
+ def installed_dependencies
106
+ spec_names = @lockfile_parser.specs.to_set(&:name)
107
+ dependencies = Bundler::Definition.build(@gemfile_path, nil, nil).dependencies
108
+
109
+ dependencies.select { |dependency| spec_names.include?(dependency.name) }
110
+ end
111
+
112
+ def gem_versions
113
+ @gem_versions ||= GemVersions.new(dependencies.map(&:name), spec_type: @spec_type)
114
+ end
115
+
116
+ def advisories_score
117
+ (1 + advisories_penalty)**-Math.log(1.09)
118
+ end
119
+
120
+ def advisories_penalty
121
+ advisories.map(&:criticality)
122
+ .sum { |criticality| advisory_penalty_map.fetch(criticality, fallback_advisory_penalty) }
123
+ end
124
+
125
+ def advisories
126
+ database = Bundler::Audit::Database.new
127
+
128
+ lockfile_parser.specs
129
+ .flat_map { |gem| database.check_gem(gem).to_a }
130
+ .reject { |advisory| ignored_advisories.intersect?(advisory.identifiers.to_set) }
131
+ end
132
+
133
+ def ignored_advisories
134
+ @ignored_advisories ||= Bundler::Audit::Configuration.load(bundler_audit_config_path).ignore.to_set
135
+ rescue Bundler::Audit::Configuration::FileNotFound, Bundler::Audit::Configuration::InvalidConfigurationError
136
+ @ignored_advisories = Set.new
137
+ end
138
+
139
+ def update_audit_database!
140
+ Bundler::Audit::Database.update!(quiet: true)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polariscope
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'polariscope/version'
4
+ require_relative 'polariscope/scanner/codebase_health_score'
5
+ require_relative 'polariscope/scanner/gem_versions'
6
+ require_relative 'polariscope/file_content'
7
+ require_relative 'polariscope/color/hsl'
8
+
9
+ module Polariscope
10
+ Error = Class.new(StandardError)
11
+
12
+ class << self
13
+ def scan(gemfile_content: nil, gemfile_lock_content: nil, bundler_audit_config_content: nil)
14
+ Scanner::CodebaseHealthScore.new(
15
+ gemfile_content: gemfile_content || FileContent.for('Gemfile'),
16
+ gemfile_lock_content: gemfile_lock_content || FileContent.for('Gemfile.lock'),
17
+ bundler_audit_config_content: bundler_audit_config_content || FileContent.for('.bundler-audit.yml')
18
+ ).health_score
19
+ end
20
+
21
+ def score_color(score)
22
+ Color::Hsl.for(score)
23
+ end
24
+
25
+ def gem_versions(dependency_names, spec_type:)
26
+ Scanner::GemVersions.new(dependency_names, spec_type: spec_type)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/polariscope/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'polariscope'
7
+ spec.version = Polariscope::VERSION
8
+ spec.authors = ['Rails team']
9
+ spec.email = ['team.rails@infinum.com']
10
+
11
+ spec.summary = 'Tool for determining the health of a project based on the state of dependencies'
12
+ spec.homepage = 'https://github.com/infinum/polariscope'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 3.0.0'
15
+
16
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/infinum/polariscope'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/infinum/polariscope/blob/master/CHANGELOG.md'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|sig)/|\.(?:git|circleci)|appveyor)})
27
+ end + Dir['exe/polariscope'] + Dir['lib/**/*']
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_dependency 'bundler'
34
+ spec.add_dependency 'bundler-audit'
35
+ spec.metadata['rubygems_mfa_required'] = 'true'
36
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polariscope
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rails team
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-07 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: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler-audit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - team.rails@infinum.com
44
+ executables:
45
+ - polariscope
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - ".ruby-version"
52
+ - CHANGELOG.md
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - exe/polariscope
59
+ - lib/polariscope.rb
60
+ - lib/polariscope/color/hsl.rb
61
+ - lib/polariscope/file_content.rb
62
+ - lib/polariscope/scanner/codebase_health_score.rb
63
+ - lib/polariscope/scanner/gem_health_score.rb
64
+ - lib/polariscope/scanner/gem_versions.rb
65
+ - lib/polariscope/scanner/gemfile_health_score.rb
66
+ - lib/polariscope/version.rb
67
+ - polariscope.gemspec
68
+ homepage: https://github.com/infinum/polariscope
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ allowed_push_host: https://rubygems.org
73
+ homepage_uri: https://github.com/infinum/polariscope
74
+ source_code_uri: https://github.com/infinum/polariscope
75
+ changelog_uri: https://github.com/infinum/polariscope/blob/master/CHANGELOG.md
76
+ rubygems_mfa_required: 'true'
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.0.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.5.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Tool for determining the health of a project based on the state of dependencies
96
+ test_files: []