spoom 1.8.11 → 1.8.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4112003bfcd62cb8c66104ac6c773669d20d4a5ab0b070e93a9d7633cdff7126
4
- data.tar.gz: 8cee75de87ce5526c8a3e6c1cf7b7d2ae7a0b98b48a1f1ea1250959984d88ca3
3
+ metadata.gz: 284bf38248b3fb6c057966d56d0bbcd01e3fa4142e6a619ffacb98eb159a6d7d
4
+ data.tar.gz: 44551f71e890cde0d0a71e77b8a4d627c7aafbddaa8e0e3a7106135c6962398a
5
5
  SHA512:
6
- metadata.gz: e23ea3cf62bc326705b91a7618efc658f7b1bcc1df17532b9f0218766cf61ab807f956751c11389db6317c1885ada0d403d51c8b92d9d0131c0ce5b0c04442c7
7
- data.tar.gz: 19bfdcbb7adbb766647ae1619450154587ccb673ad9d84b7a1f070dd3f1f9c9bef3d38f233df3b4f914e7a786832bbfe1b4fd51f972a2a4873c626a0250abbb9
6
+ metadata.gz: 60c251eaa56b3ad7ffac08afe381203869146040b144c7893013dcb098fc9150bad9a05b49bc303919946904b0757a6ddd285282a80bf64f607630d942b57d19
7
+ data.tar.gz: 462f445ffb6b740ef17486e52d3e3c65d000f482199206ea3ef6e39c7a9aa7183c9df8097b73967f89f3db5c88d3e751edba61c3fb691f2909a48930a0d78b5e
data/README.md CHANGED
@@ -387,6 +387,20 @@ Remove a reported dead code candidate:
387
387
  spoom deadcode remove path/to/file.rb:42:18-47:23
388
388
  ```
389
389
 
390
+ #### Custom plugins
391
+
392
+ Teach `spoom deadcode` about your project's own DSLs by adding plugins to `.spoom/deadcode/plugins/*.rb`. Each file should define a subclass of `Spoom::Deadcode::Plugins::Base`:
393
+
394
+ ```ruby
395
+ # .spoom/deadcode/plugins/my_plugin.rb
396
+ class MyPlugin < Spoom::Deadcode::Plugins::Base
397
+ ignore_methods_named("my_dsl_method")
398
+ end
399
+ ```
400
+
401
+ > [!WARNING]
402
+ > Custom plugins are loaded with `require`, so `spoom deadcode` executes any Ruby code in `.spoom/deadcode/plugins/` of the project being analyzed. Only run `spoom deadcode` on projects you trust, and inspect or remove `.spoom/deadcode/plugins/` before analyzing untrusted code (such as a pull request from an outside contributor).
403
+
390
404
  ## Ruby API
391
405
 
392
406
  ### Parsing Sorbet config
@@ -73,7 +73,18 @@ module Spoom
73
73
  end
74
74
 
75
75
  plugin_classes = Spoom::Deadcode.plugins_from_gemfile_lock(context)
76
- plugin_classes.merge(Spoom::Deadcode.load_custom_plugins(context))
76
+
77
+ custom_plugin_files = context.glob("#{Spoom::Deadcode::DEFAULT_CUSTOM_PLUGINS_PATH}/*.rb")
78
+ unless custom_plugin_files.empty?
79
+ say_warning(
80
+ "Loading custom plugins from `#{Spoom::Deadcode::DEFAULT_CUSTOM_PLUGINS_PATH}`. " \
81
+ "This executes Ruby code from the analyzed project:",
82
+ )
83
+ custom_plugin_files.each do |file|
84
+ $stderr.puts " #{gray(file)}"
85
+ end
86
+ plugin_classes.merge(Spoom::Deadcode.load_custom_plugins(context))
87
+ end
77
88
  if options[:show_plugins]
78
89
  $stderr.puts "\nLoaded #{blue(plugin_classes.size.to_s)} plugins\n"
79
90
  plugin_classes.each do |plugin|
@@ -153,6 +153,9 @@ module Spoom
153
153
  snapshots = files.sort.map do |file|
154
154
  json = File.read(file)
155
155
  Spoom::Coverage::Snapshot.from_json(json)
156
+ rescue Spoom::Coverage::Snapshot::Error => e
157
+ say_error("Invalid snapshot `#{file}`: #{e.message}")
158
+ exit(1)
156
159
  end.filter(&:commit_timestamp).sort_by!(&:commit_timestamp)
157
160
 
158
161
  palette = Spoom::Coverage::D3::ColorPalette.new(
@@ -6,6 +6,9 @@ module Spoom
6
6
  module D3
7
7
  # @abstract
8
8
  class Base
9
+ # Characters that must not appear raw inside a `<script>` element, and their JSON string escapes
10
+ JS_LITERAL_ESCAPES = { "<" => '\u003c', ">" => '\u003e', "&" => '\u0026' }.freeze #: Hash[String, String]
11
+
9
12
  #: String
10
13
  attr_reader :id
11
14
 
@@ -43,6 +46,18 @@ module Spoom
43
46
  # @abstract
44
47
  #: -> String
45
48
  def script = raise NotImplementedError, "Abstract method called"
49
+
50
+ private
51
+
52
+ # Serialize `value` as a JavaScript literal that is safe to embed in a `<script>` tag
53
+ #
54
+ # `script_safe` escapes `/` so the data can't contain `</script>`. We also escape `<`, `>` and `&`
55
+ # so the data can't contain `<!--` or `<script`, which change how the HTML parser reads the rest
56
+ # of the `<script>` element.
57
+ #: (untyped value) -> String
58
+ def js_literal(value)
59
+ JSON.generate(value, script_safe: true).gsub(/[<>&]/, JS_LITERAL_ESCAPES)
60
+ end
46
61
  end
47
62
  end
48
63
  end
@@ -47,7 +47,7 @@ module Spoom
47
47
 
48
48
  function tooltipMap(d) {
49
49
  moveTooltip(d)
50
- .html("<b>" + d.data.name + "</b>")
50
+ .html("<b>" + escapeHtml(d.data.name) + "</b>")
51
51
  }
52
52
  JS
53
53
  end
@@ -57,7 +57,7 @@ module Spoom
57
57
  #: -> String
58
58
  def script
59
59
  <<~JS
60
- var root = {children: #{@data.to_json}}
60
+ var root = {children: #{js_literal(@data)}}
61
61
  var dataHeight = treeHeight(root)
62
62
 
63
63
  var opacity = d3.scaleLinear()
@@ -38,8 +38,8 @@ module Spoom
38
38
  <<~JS
39
39
  function tooltipPie(d, title, kind, sum) {
40
40
  moveTooltip(d)
41
- .html("<b>" + title + "</b><br><br>"
42
- + "<b>" + d.data.value + "</b> " + kind + "<br>"
41
+ .html("<b>" + escapeHtml(title) + "</b><br><br>"
42
+ + "<b>" + escapeHtml(d.data.value) + "</b> " + kind + "<br>"
43
43
  + "<b>" + toPercent(d.data.value, sum) + "</b>%")
44
44
  }
45
45
  JS
@@ -52,11 +52,11 @@ module Spoom
52
52
  <<~JS
53
53
  #{tooltip}
54
54
 
55
- var json_#{id} = #{@data.to_json};
55
+ var json_#{id} = #{js_literal(@data)};
56
56
  var pie_#{id} = d3.pie().value((d) => d.value);
57
57
  var data_#{id} = pie_#{id}(d3.entries(json_#{id}));
58
58
  var sum_#{id} = d3.sum(data_#{id}, (d) => d.data.value);
59
- var title_#{id} = #{@title.to_json};
59
+ var title_#{id} = #{js_literal(@title)};
60
60
 
61
61
  function draw_#{id}() {
62
62
  var pieSize_#{id} = document.getElementById("#{id}").clientWidth - 10;
@@ -81,10 +81,10 @@ module Spoom
81
81
 
82
82
  function tooltipTimeline(d, kind) {
83
83
  moveTooltip(d)
84
- .html("commit <b>" + d.data.commit + "</b><br>"
84
+ .html("commit <b>" + escapeHtml(d.data.commit) + "</b><br>"
85
85
  + d3.timeFormat("%y/%m/%d")(parseDate(d.data.timestamp)) + "<br><br>"
86
- + "<b>typed: " + d.key + "</b><br><br>"
87
- + "<b>" + (d.data.values[d.key] ? d.data.values[d.key] : 0) + "</b> " + kind +"<br>"
86
+ + "<b>typed: " + escapeHtml(d.key) + "</b><br><br>"
87
+ + "<b>" + escapeHtml(d.data.values[d.key] ? d.data.values[d.key] : 0) + "</b> " + kind +"<br>"
88
88
  + "<b>" + toPercent(d.data.values[d.key] ? d.data.values[d.key] : 0, d.data.total) + "%")
89
89
  }
90
90
  JS
@@ -97,7 +97,7 @@ module Spoom
97
97
  <<~HTML
98
98
  #{tooltip}
99
99
 
100
- var data_#{id} = #{@data.to_json};
100
+ var data_#{id} = #{js_literal(@data)};
101
101
 
102
102
  function draw_#{id}() {
103
103
  var width_#{id} = document.getElementById("#{id}").clientWidth;
@@ -245,10 +245,10 @@ module Spoom
245
245
  <<~JS
246
246
  function tooltip_#{id}(d) {
247
247
  moveTooltip(d)
248
- .html("commit <b>" + d.commit + "</b><br>"
248
+ .html("commit <b>" + escapeHtml(d.commit) + "</b><br>"
249
249
  + d3.timeFormat("%y/%m/%d")(parseDate(d.timestamp)) + "<br><br>"
250
- + "static: v<b>" + d.static + "</b><br>"
251
- + "runtime: v<b>" + d.runtime + "</b><br><br>"
250
+ + "static: v<b>" + escapeHtml(d.static) + "</b><br>"
251
+ + "runtime: v<b>" + escapeHtml(d.runtime) + "</b><br><br>"
252
252
  + "versions from<br>Gemfile.lock")
253
253
  }
254
254
  JS
@@ -294,9 +294,9 @@ module Spoom
294
294
  <<~JS
295
295
  function tooltip_#{id}(d) {
296
296
  moveTooltip(d)
297
- .html("commit <b>" + d.commit + "</b><br>"
297
+ .html("commit <b>" + escapeHtml(d.commit) + "</b><br>"
298
298
  + d3.timeFormat("%y/%m/%d")(parseDate(d.timestamp)) + "<br><br>"
299
- + "<b>" + d.runtime + "</b>s<br><br>"
299
+ + "<b>" + escapeHtml(d.runtime) + "</b>s<br><br>"
300
300
  + "(sorbet user + system time)")
301
301
  }
302
302
  JS
@@ -330,8 +330,8 @@ module Spoom
330
330
  <<~JS
331
331
  #{tooltip}
332
332
 
333
- var data_#{id} = #{@data.to_json};
334
- var keys_#{id} = #{T.unsafe(@keys).to_json};
333
+ var data_#{id} = #{js_literal(@data)};
334
+ var keys_#{id} = #{js_literal(@keys)};
335
335
 
336
336
  var stack_#{id} = d3.stack()
337
337
  .keys(keys_#{id})
@@ -515,11 +515,11 @@ module Spoom
515
515
  <<~JS
516
516
  function tooltip_#{id}(d) {
517
517
  moveTooltip(d)
518
- .html("commit <b>" + d.data.commit + "</b><br>"
518
+ .html("commit <b>" + escapeHtml(d.data.commit) + "</b><br>"
519
519
  + d3.timeFormat("%y/%m/%d")(parseDate(d.data.timestamp)) + "<br><br>"
520
- + "Files: <b>" + d.data.values.files + "</b><br>"
521
- + "RBIs: <b>" + d.data.values.rbis + "</b><br><br>"
522
- + "Total: <b>" + d.data.total + "</b>")
520
+ + "Files: <b>" + escapeHtml(d.data.values.files) + "</b><br>"
521
+ + "RBIs: <b>" + escapeHtml(d.data.values.rbis) + "</b><br><br>"
522
+ + "Total: <b>" + escapeHtml(d.data.total) + "</b>")
523
523
  }
524
524
  JS
525
525
  end
@@ -530,8 +530,8 @@ module Spoom
530
530
  <<~JS
531
531
  #{tooltip}
532
532
 
533
- var data_#{id} = #{@data.to_json};
534
- var keys_#{id} = #{T.unsafe(@keys).to_json};
533
+ var data_#{id} = #{js_literal(@data)};
534
+ var keys_#{id} = #{js_literal(@keys)};
535
535
 
536
536
  var stack_#{id} = d3.stack()
537
537
  .keys(keys_#{id})
@@ -76,6 +76,15 @@ module Spoom
76
76
  return "#{palette.false_color}";
77
77
  }
78
78
 
79
+ function escapeHtml(value) {
80
+ return String(value)
81
+ .replace(/&/g, "&amp;")
82
+ .replace(/</g, "&lt;")
83
+ .replace(/>/g, "&gt;")
84
+ .replace(/"/g, "&quot;")
85
+ .replace(/'/g, "&#39;");
86
+ }
87
+
79
88
  function toPercent(value, sum) {
80
89
  return value ? Math.round(value * 100 / sum) : 0;
81
90
  }
@@ -60,7 +60,7 @@ module Spoom
60
60
 
61
61
  #: -> String
62
62
  def header_html
63
- "<h1 class='display-3'>#{title}</h1>"
63
+ "<h1 class='display-3'>#{ERB::Util.html_escape(title)}</h1>"
64
64
  end
65
65
 
66
66
  #: -> String
@@ -219,7 +219,7 @@ module Spoom
219
219
  <<~ERB
220
220
  <div class="text-center" style="margin-top: 30px">
221
221
  Typchecked by Sorbet since <b>#{@sorbet_intro_date&.strftime("%F")}</b>
222
- (commit <b>#{@sorbet_intro_commit}</b>).
222
+ (commit <b>#{ERB::Util.html_escape(@sorbet_intro_commit)}</b>).
223
223
  </div>
224
224
  ERB
225
225
  end
@@ -263,8 +263,8 @@ module Spoom
263
263
  last = T.must(@snapshots.last)
264
264
  <<~ERB
265
265
  <h1 class="display-3">
266
- #{@project_name}
267
- <span class="badge badge-pill badge-dark" style="font-size: 20%;">#{last.commit_sha}</span>
266
+ #{ERB::Util.html_escape(@project_name)}
267
+ <span class="badge badge-pill badge-dark" style="font-size: 20%;">#{ERB::Util.html_escape(last.commit_sha)}</span>
268
268
  </h1>
269
269
  ERB
270
270
  end
@@ -4,6 +4,8 @@
4
4
  module Spoom
5
5
  module Coverage
6
6
  class Snapshot
7
+ class Error < Spoom::Error; end
8
+
7
9
  #: Integer
8
10
  attr_accessor :timestamp
9
11
 
@@ -173,30 +175,32 @@ module Spoom
173
175
  #: (Hash[String, untyped] obj) -> Snapshot
174
176
  def from_obj(obj)
175
177
  snapshot = Snapshot.new
176
- snapshot.timestamp = obj.fetch("timestamp", 0)
177
- snapshot.version_static = obj.fetch("version_static", nil)
178
- snapshot.version_runtime = obj.fetch("version_runtime", nil)
179
- snapshot.duration = obj.fetch("duration", 0)
180
- snapshot.commit_sha = obj.fetch("commit_sha", nil)
181
- snapshot.commit_timestamp = obj.fetch("commit_timestamp", nil)
182
- snapshot.files = obj.fetch("files", 0)
183
- snapshot.rbi_files = obj.fetch("rbi_files", 0)
184
- snapshot.modules = obj.fetch("modules", 0)
185
- snapshot.classes = obj.fetch("classes", 0)
186
- snapshot.singleton_classes = obj.fetch("singleton_classes", 0)
187
- snapshot.methods_with_sig = obj.fetch("methods_with_sig", 0)
188
- snapshot.methods_without_sig = obj.fetch("methods_without_sig", 0)
189
- snapshot.calls_typed = obj.fetch("calls_typed", 0)
190
- snapshot.calls_untyped = obj.fetch("calls_untyped", 0)
191
- snapshot.methods_with_sig_excluding_rbis = obj.fetch("methods_with_sig_excluding_rbis", 0)
192
- snapshot.methods_without_sig_excluding_rbis = obj.fetch("methods_without_sig_excluding_rbis", 0)
178
+ snapshot.timestamp = integer_field(obj, "timestamp")
179
+ snapshot.version_static = obj.fetch("version_static", nil)&.to_s
180
+ snapshot.version_runtime = obj.fetch("version_runtime", nil)&.to_s
181
+ snapshot.duration = integer_field(obj, "duration")
182
+ snapshot.commit_sha = obj.fetch("commit_sha", nil)&.to_s
183
+ unless obj.fetch("commit_timestamp", nil).nil?
184
+ snapshot.commit_timestamp = integer_field(obj, "commit_timestamp")
185
+ end
186
+ snapshot.files = integer_field(obj, "files")
187
+ snapshot.rbi_files = integer_field(obj, "rbi_files")
188
+ snapshot.modules = integer_field(obj, "modules")
189
+ snapshot.classes = integer_field(obj, "classes")
190
+ snapshot.singleton_classes = integer_field(obj, "singleton_classes")
191
+ snapshot.methods_with_sig = integer_field(obj, "methods_with_sig")
192
+ snapshot.methods_without_sig = integer_field(obj, "methods_without_sig")
193
+ snapshot.calls_typed = integer_field(obj, "calls_typed")
194
+ snapshot.calls_untyped = integer_field(obj, "calls_untyped")
195
+ snapshot.methods_with_sig_excluding_rbis = integer_field(obj, "methods_with_sig_excluding_rbis")
196
+ snapshot.methods_without_sig_excluding_rbis = integer_field(obj, "methods_without_sig_excluding_rbis")
193
197
 
194
198
  sigils = obj.fetch("sigils", {})
195
199
  if sigils
196
200
  Snapshot::STRICTNESSES.each do |strictness|
197
201
  next unless sigils.key?(strictness)
198
202
 
199
- snapshot.sigils[strictness] = sigils[strictness]
203
+ snapshot.sigils[strictness] = integer_field(sigils, strictness)
200
204
  end
201
205
  end
202
206
 
@@ -205,12 +209,29 @@ module Spoom
205
209
  Snapshot::STRICTNESSES.each do |strictness|
206
210
  next unless sigils_excluding_rbis.key?(strictness)
207
211
 
208
- snapshot.sigils_excluding_rbis[strictness] = sigils_excluding_rbis[strictness]
212
+ snapshot.sigils_excluding_rbis[strictness] = integer_field(sigils_excluding_rbis, strictness)
209
213
  end
210
214
  end
211
215
 
212
216
  snapshot
213
217
  end
218
+
219
+ private
220
+
221
+ # Read `key` from `obj` as an Integer
222
+ #
223
+ # Snapshot files live in the analyzed project and can't be trusted to hold the types spoom wrote.
224
+ # Raises `Snapshot::Error` if the value can't be interpreted as an Integer.
225
+ #: (Hash[String, untyped] obj, String key, ?default: Integer) -> Integer
226
+ def integer_field(obj, key, default: 0)
227
+ if (value = obj[key])
228
+ Integer(value)
229
+ else
230
+ default
231
+ end
232
+ rescue ArgumentError, TypeError
233
+ raise Error, "Expected an integer for `#{key}`, got #{value.inspect}"
234
+ end
214
235
  end
215
236
  end
216
237
 
data/lib/spoom/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.8.11"
5
+ VERSION = "1.8.12"
6
6
  end
data/rbi/spoom.rbi CHANGED
@@ -614,6 +614,11 @@ class Spoom::Coverage::D3::Base
614
614
  sig { returns(::String) }
615
615
  def tooltip; end
616
616
 
617
+ private
618
+
619
+ sig { params(value: T.untyped).returns(::String) }
620
+ def js_literal(value); end
621
+
617
622
  class << self
618
623
  sig { returns(::String) }
619
624
  def header_script; end
@@ -623,6 +628,7 @@ class Spoom::Coverage::D3::Base
623
628
  end
624
629
  end
625
630
 
631
+ Spoom::Coverage::D3::Base::JS_LITERAL_ESCAPES = T.let(T.unsafe(nil), Hash)
626
632
  Spoom::Coverage::D3::COLOR_FALSE = T.let(T.unsafe(nil), String)
627
633
  Spoom::Coverage::D3::COLOR_IGNORE = T.let(T.unsafe(nil), String)
628
634
  Spoom::Coverage::D3::COLOR_STRICT = T.let(T.unsafe(nil), String)
@@ -1070,9 +1076,15 @@ class Spoom::Coverage::Snapshot
1070
1076
 
1071
1077
  sig { params(obj: T::Hash[::String, T.untyped]).returns(::Spoom::Coverage::Snapshot) }
1072
1078
  def from_obj(obj); end
1079
+
1080
+ private
1081
+
1082
+ sig { params(obj: T::Hash[::String, T.untyped], key: ::String, default: ::Integer).returns(::Integer) }
1083
+ def integer_field(obj, key, default: T.unsafe(nil)); end
1073
1084
  end
1074
1085
  end
1075
1086
 
1087
+ class Spoom::Coverage::Snapshot::Error < ::Spoom::Error; end
1076
1088
  Spoom::Coverage::Snapshot::STRICTNESSES = T.let(T.unsafe(nil), Array)
1077
1089
 
1078
1090
  class Spoom::Coverage::SnapshotPrinter < ::Spoom::Printer
data/templates/page.erb CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta http-equiv="x-ua-compatible" content="ie=edge" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
 
8
- <title><%= title %></title>
8
+ <title><%= ERB::Util.html_escape(title) %></title>
9
9
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
10
10
 
11
11
  <style>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.11
4
+ version: 1.8.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Terrasa