spoom 1.1.11 → 1.1.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b239fa524447d3a3cd6290f8fa373cdaf9a1b3b76bacbbf290519820fe31f542
4
- data.tar.gz: 389b772cafcc6658f6033707ec1696cc1221aab64efc8c0c4bef984e672e1ea4
3
+ metadata.gz: ed6d04e01efc1c6c8c00df77ad340f189a277aebf3afbd64edbd89f6145541f2
4
+ data.tar.gz: 1935557277bcfdb54b4ab0d389ab5a72fb5b2d00a6631f6c787dae9d83dac7c4
5
5
  SHA512:
6
- metadata.gz: df641a876db23dd5aac45965adbae953ba43ae2fb20720498d6760843212a3541b4d3396401ad9cccb99db58830d3bec7124e32a27372496c61fd896517be126
7
- data.tar.gz: 4f1b7fe4cd814333c449820bbd24f4fd3ca4883986b3d6f3c0c6f59dada06e136114930a9e8e20a72beb85854ef5f240d7dd6d6c0c5c0c28f5b70fc01185045a
6
+ metadata.gz: 781d3bb6443c518fc1d4334e5c4ac17a09372ba9115f024bcdceb5cf15af4d4524d1e4cb465963ce5d23aaa07ebd5ab2114ebe81b4eea3fffe27942295e21b9f
7
+ data.tar.gz: 3427fdfe5cd483609fbbe6618de2f8a536f8bf0727a6865e44c2c4c9b74f9303c0fdcfdefd44bdd547d16e6766f0f09fa49c1e8cd991d0efcffaa37f3c9c7609
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gemspec
7
7
 
8
8
  group :development do
9
9
  gem "pry-byebug"
10
+ gem "ruby-lsp"
10
11
  gem "rubocop-shopify", require: false
11
12
  gem "rubocop-sorbet", require: false
12
13
  gem "tapioca", require: false
data/README.md CHANGED
@@ -121,6 +121,12 @@ Hide the `Errors: X` at the end of the list:
121
121
  $ spoom tc --no-count
122
122
  ```
123
123
 
124
+ List only the errors comming from specific directories or files:
125
+
126
+ ```
127
+ $ spoom tc file1.rb path1/ path2/
128
+ ```
129
+
124
130
  #### Typing coverage
125
131
 
126
132
  Show metrics about the project contents and the typing coverage:
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
+
3
4
  require "bundler/gem_tasks"
4
5
  require "rake/testtask"
5
6
 
@@ -1,8 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'find'
5
- require 'open3'
4
+ require "find"
5
+ require "open3"
6
6
 
7
7
  module Spoom
8
8
  module Cli
@@ -85,7 +85,6 @@ module Spoom
85
85
 
86
86
  error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
87
87
  result = Sorbet.srb_tc(
88
- "--no-error-sections",
89
88
  "--error-url-base=#{error_url_base}",
90
89
  path: exec_path,
91
90
  capture_err: true,
@@ -108,17 +107,22 @@ module Spoom
108
107
 
109
108
  errors = Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
110
109
 
111
- files_with_errors = errors.map do |err|
112
- path = File.expand_path(err.file)
110
+ all_files = errors.flat_map do |err|
111
+ [err.file, *err.files_from_error_sections]
112
+ end
113
+
114
+ files_with_errors = all_files.map do |file|
115
+ path = File.expand_path(file)
113
116
  next unless path.start_with?(directory)
114
117
  next unless File.file?(path)
115
118
  next unless files_to_bump.include?(path)
119
+
116
120
  path
117
121
  end.compact.uniq
118
122
 
119
123
  undo_changes(files_with_errors, from)
120
124
 
121
- say("Found #{errors.length} type checking error#{'s' if errors.length > 1}") if options[:count_errors]
125
+ say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors]
122
126
 
123
127
  files_changed = files_to_bump - files_with_errors
124
128
  print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
@@ -134,7 +138,7 @@ module Spoom
134
138
  end
135
139
  message = StringIO.new
136
140
  message << (dry ? "Can bump" : "Bumped")
137
- message << " `#{files.size}` file#{'s' if files.size > 1}"
141
+ message << " `#{files.size}` file#{"s" if files.size > 1}"
138
142
  message << " from `#{from}` to `#{to}`:"
139
143
  say(message.string)
140
144
  files.each do |file|
@@ -1,8 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../coverage'
5
- require_relative '../timeline'
4
+ require_relative "../coverage"
5
+ require_relative "../timeline"
6
6
 
7
7
  module Spoom
8
8
  module Cli
@@ -27,6 +27,7 @@ module Spoom
27
27
 
28
28
  save_dir = options[:save]
29
29
  return unless save_dir
30
+
30
31
  FileUtils.mkdir_p(save_dir)
31
32
  file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
32
33
  File.write(file, snapshot.to_json)
@@ -85,7 +86,7 @@ module Spoom
85
86
 
86
87
  ticks.each_with_index do |sha, i|
87
88
  date = Spoom::Git.commit_time(sha, path: path)
88
- say("Analyzing commit `#{sha}` - #{date&.strftime('%F')} (#{i + 1} / #{ticks.size})")
89
+ say("Analyzing commit `#{sha}` - #{date&.strftime("%F")} (#{i + 1} / #{ticks.size})")
89
90
 
90
91
  Spoom::Git.checkout(sha, path: path)
91
92
 
@@ -93,6 +94,7 @@ module Spoom
93
94
  if options[:bundle_install]
94
95
  Bundler.with_unbundled_env do
95
96
  next unless bundle_install(path, sha)
97
+
96
98
  snapshot = Spoom::Coverage.snapshot(path: path, sorbet_bin: sorbet)
97
99
  end
98
100
  else
@@ -104,6 +106,7 @@ module Spoom
104
106
  say("\n")
105
107
 
106
108
  next unless save_dir
109
+
107
110
  file = "#{save_dir}/#{sha}.json"
108
111
  File.write(file, snapshot.to_json)
109
112
  say(" Snapshot data saved under `#{file}`\n\n")
@@ -161,9 +164,9 @@ module Spoom
161
164
  say_error("No report file to open `#{file}`")
162
165
  say_error(<<~ERR, status: nil)
163
166
 
164
- If you already generated a report under another name use #{blue('spoom coverage open PATH')}.
167
+ If you already generated a report under another name use #{blue("spoom coverage open PATH")}.
165
168
 
166
- To generate a report run #{blue('spoom coverage report')}.
169
+ To generate a report run #{blue("spoom coverage report")}.
167
170
  ERR
168
171
  exit(1)
169
172
  end
@@ -174,6 +177,7 @@ module Spoom
174
177
  no_commands do
175
178
  def parse_time(string, option)
176
179
  return nil unless string
180
+
177
181
  Time.parse(string)
178
182
  rescue ArgumentError
179
183
  say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
@@ -196,9 +200,9 @@ module Spoom
196
200
  say_error("No snapshot files found in `#{file}`")
197
201
  say_error(<<~ERR, status: nil)
198
202
 
199
- If you already generated snapshot files under another directory use #{blue('spoom coverage report PATH')}.
203
+ If you already generated snapshot files under another directory use #{blue("spoom coverage report PATH")}.
200
204
 
201
- To generate snapshot files run #{blue('spoom coverage timeline --save-dir spoom_data')}.
205
+ To generate snapshot files run #{blue("spoom coverage timeline --save")}.
202
206
  ERR
203
207
  end
204
208
  end
@@ -60,8 +60,8 @@ module Spoom
60
60
  unless in_sorbet_project?
61
61
  say_error(
62
62
  "not in a Sorbet project (`#{sorbet_config_file}` not found)\n\n" \
63
- "When running spoom from another path than the project's root, " \
64
- "use `--path PATH` to specify the path to the root."
63
+ "When running spoom from another path than the project's root, " \
64
+ "use `--path PATH` to specify the path to the root."
65
65
  )
66
66
  Kernel.exit(1)
67
67
  end
@@ -116,9 +116,9 @@ module Spoom
116
116
  word = StringIO.new
117
117
  in_ticks = T.let(false, T::Boolean)
118
118
  string.chars.each do |c|
119
- if c == '`' && !in_ticks
119
+ if c == "`" && !in_ticks
120
120
  in_ticks = true
121
- elsif c == '`' && in_ticks
121
+ elsif c == "`" && in_ticks
122
122
  in_ticks = false
123
123
  res << colorize(word.string, HIGHLIGHT_COLOR)
124
124
  word = StringIO.new
@@ -135,6 +135,7 @@ module Spoom
135
135
  sig { params(string: String, color: Color).returns(String) }
136
136
  def colorize(string, *color)
137
137
  return string unless color?
138
+
138
139
  T.unsafe(self).set_color(string, *color)
139
140
  end
140
141
 
data/lib/spoom/cli/lsp.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'shellwords'
4
+ require "shellwords"
5
5
 
6
6
  require_relative "../sorbet/lsp"
7
7
 
@@ -28,6 +28,7 @@ module Spoom
28
28
  Dir["**/*.rb"].each do |file|
29
29
  res = client.document_symbols(to_uri(file))
30
30
  next if res.empty?
31
+
31
32
  say("Symbols from `#{file}`:")
32
33
  printer.print_objects(res)
33
34
  end
data/lib/spoom/cli/run.rb CHANGED
@@ -22,7 +22,8 @@ module Spoom
22
22
  option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
23
  option :count, type: :boolean, default: true, desc: "Show errors count"
24
24
  option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
25
- def tc(*arg)
25
+ option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
26
+ def tc(*paths_to_select)
26
27
  in_sorbet_project!
27
28
 
28
29
  path = exec_path
@@ -36,7 +37,7 @@ module Spoom
36
37
 
37
38
  unless limit || code || sort
38
39
  result = T.unsafe(Spoom::Sorbet).srb_tc(
39
- *arg,
40
+ *options[:sorbet_options].split(" "),
40
41
  path: path,
41
42
  capture_err: false,
42
43
  sorbet_bin: sorbet
@@ -47,8 +48,10 @@ module Spoom
47
48
  exit(result.status)
48
49
  end
49
50
 
51
+ error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
50
52
  result = T.unsafe(Spoom::Sorbet).srb_tc(
51
- *arg,
53
+ *options[:sorbet_options].split(" "),
54
+ "--error-url-base=#{error_url_base}",
52
55
  path: path,
53
56
  capture_err: true,
54
57
  sorbet_bin: sorbet
@@ -61,9 +64,17 @@ module Spoom
61
64
  exit(0)
62
65
  end
63
66
 
64
- errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err)
67
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
65
68
  errors_count = errors.size
66
69
 
70
+ errors = errors.select { |e| e.code == code } if code
71
+
72
+ unless paths_to_select.empty?
73
+ errors.select! do |error|
74
+ paths_to_select.any? { |path_to_select| error.file&.start_with?(path_to_select) }
75
+ end
76
+ end
77
+
67
78
  errors = case sort
68
79
  when SORT_CODE
69
80
  Spoom::Sorbet::Errors.sort_errors_by_code(errors)
@@ -73,7 +84,6 @@ module Spoom
73
84
  errors # preserve natural sort
74
85
  end
75
86
 
76
- errors = errors.select { |e| e.code == code } if code
77
87
  errors = T.must(errors.slice(0, limit)) if limit
78
88
 
79
89
  lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
@@ -110,7 +120,7 @@ module Spoom
110
120
  cyan = T.let(false, T::Boolean)
111
121
  word = StringIO.new
112
122
  message.chars.each do |c|
113
- if c == '`'
123
+ if c == "`"
114
124
  cyan = !cyan
115
125
  next
116
126
  end
data/lib/spoom/cli.rb CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "thor"
5
5
 
6
- require_relative 'cli/helper'
6
+ require_relative "cli/helper"
7
7
 
8
8
  require_relative "cli/bump"
9
9
  require_relative "cli/config"
@@ -20,7 +20,7 @@ module Spoom
20
20
  class_option :color, type: :boolean, default: true, desc: "Use colors"
21
21
  class_option :path, type: :string, default: ".", aliases: :p, desc: "Run spoom in a specific path"
22
22
 
23
- map T.unsafe(%w[--version -v] => :__print_version)
23
+ map T.unsafe(["--version", "-v"] => :__print_version)
24
24
 
25
25
  desc "bump", "Bump Sorbet sigils from `false` to `true` when no errors"
26
26
  subcommand "bump", Spoom::Cli::Bump
@@ -71,8 +71,10 @@ module Spoom
71
71
 
72
72
  # Utils
73
73
 
74
- def self.exit_on_failure?
75
- true
74
+ class << self
75
+ def exit_on_failure?
76
+ true
77
+ end
76
78
  end
77
79
  end
78
80
  end
@@ -0,0 +1,219 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "open3"
6
+
7
+ module Spoom
8
+ # An abstraction to a Ruby project context
9
+ #
10
+ # A context maps to a directory in the file system.
11
+ # It is used to manipulate files and run commands in the context of this directory.
12
+ class Context
13
+ extend T::Sig
14
+
15
+ # The absolute path to the directory this context is about
16
+ sig { returns(String) }
17
+ attr_reader :absolute_path
18
+
19
+ class << self
20
+ extend T::Sig
21
+
22
+ # Create a new context in the system's temporary directory
23
+ #
24
+ # `name` is used as prefix to the temporary directory name.
25
+ # The directory will be created if it doesn't exist.
26
+ sig { params(name: T.nilable(String)).returns(T.attached_class) }
27
+ def mktmp!(name = nil)
28
+ new(::Dir.mktmpdir(name))
29
+ end
30
+ end
31
+
32
+ # Create a new context about `absolute_path`
33
+ #
34
+ # The directory will not be created if it doesn't exist.
35
+ # Call `#make!` to create it.
36
+ sig { params(absolute_path: String).void }
37
+ def initialize(absolute_path)
38
+ @absolute_path = T.let(::File.expand_path(absolute_path), String)
39
+ end
40
+
41
+ # Returns the absolute path to `relative_path` in the context's directory
42
+ sig { params(relative_path: String).returns(String) }
43
+ def absolute_path_to(relative_path)
44
+ File.join(@absolute_path, relative_path)
45
+ end
46
+
47
+ # File System
48
+
49
+ # Create the context directory at `absolute_path`
50
+ sig { void }
51
+ def mkdir!
52
+ FileUtils.rm_rf(@absolute_path)
53
+ FileUtils.mkdir_p(@absolute_path)
54
+ end
55
+
56
+ # List all files in this context matching `pattern`
57
+ sig { params(pattern: String).returns(T::Array[String]) }
58
+ def glob(pattern = "**/*")
59
+ Dir.glob(absolute_path_to(pattern)).map do |path|
60
+ Pathname.new(path).relative_path_from(@absolute_path).to_s
61
+ end.sort
62
+ end
63
+
64
+ # List all files at the top level of this context directory
65
+ sig { returns(T::Array[String]) }
66
+ def list
67
+ glob("*")
68
+ end
69
+
70
+ # Does `relative_path` point to an existing file in this context directory?
71
+ sig { params(relative_path: String).returns(T::Boolean) }
72
+ def file?(relative_path)
73
+ File.file?(absolute_path_to(relative_path))
74
+ end
75
+
76
+ # Return the contents of the file at `relative_path` in this context directory
77
+ #
78
+ # Will raise if the file doesn't exist.
79
+ sig { params(relative_path: String).returns(String) }
80
+ def read(relative_path)
81
+ File.read(absolute_path_to(relative_path))
82
+ end
83
+
84
+ # Write `contents` in the file at `relative_path` in this context directory
85
+ #
86
+ # Append to the file if `append` is true.
87
+ sig { params(relative_path: String, contents: String, append: T::Boolean).void }
88
+ def write!(relative_path, contents = "", append: false)
89
+ absolute_path = absolute_path_to(relative_path)
90
+ FileUtils.mkdir_p(File.dirname(absolute_path))
91
+ File.write(absolute_path, contents, mode: append ? "a" : "w")
92
+ end
93
+
94
+ # Remove the path at `relative_path` (recursive + force) in this context directory
95
+ sig { params(relative_path: String).void }
96
+ def remove!(relative_path)
97
+ FileUtils.rm_rf(absolute_path_to(relative_path))
98
+ end
99
+
100
+ # Move the file or directory from `from_relative_path` to `to_relative_path`
101
+ sig { params(from_relative_path: String, to_relative_path: String).void }
102
+ def move!(from_relative_path, to_relative_path)
103
+ destination_path = absolute_path_to(to_relative_path)
104
+ FileUtils.mkdir_p(File.dirname(destination_path))
105
+ FileUtils.mv(absolute_path_to(from_relative_path), destination_path)
106
+ end
107
+
108
+ # Delete this context and its content
109
+ #
110
+ # Warning: it will `rm -rf` the context directory on the file system.
111
+ sig { void }
112
+ def destroy!
113
+ FileUtils.rm_rf(@absolute_path)
114
+ end
115
+
116
+ # Execution
117
+
118
+ # Run a command in this context directory
119
+ sig { params(command: String, capture_err: T::Boolean).returns(ExecResult) }
120
+ def exec(command, capture_err: true)
121
+ Bundler.with_unbundled_env do
122
+ opts = T.let({ chdir: @absolute_path }, T::Hash[Symbol, T.untyped])
123
+
124
+ if capture_err
125
+ out, err, status = Open3.capture3(command, opts)
126
+ ExecResult.new(out: out, err: err, status: T.must(status.success?), exit_code: T.must(status.exitstatus))
127
+ else
128
+ out, status = Open3.capture2(command, opts)
129
+ ExecResult.new(out: out, err: "", status: T.must(status.success?), exit_code: T.must(status.exitstatus))
130
+ end
131
+ end
132
+ end
133
+
134
+ # Bundle
135
+
136
+ # Read the `contents` of the Gemfile in this context directory
137
+ sig { returns(T.nilable(String)) }
138
+ def read_gemfile
139
+ read("Gemfile")
140
+ end
141
+
142
+ # Set the `contents` of the Gemfile in this context directory
143
+ sig { params(contents: String, append: T::Boolean).void }
144
+ def write_gemfile!(contents, append: false)
145
+ write!("Gemfile", contents, append: append)
146
+ end
147
+
148
+ # Run a command with `bundle` in this context directory
149
+ sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
150
+ def bundle(command, version: nil)
151
+ command = "_#{version}_ #{command}" if version
152
+ exec("bundle #{command}")
153
+ end
154
+
155
+ # Run `bundle install` in this context directory
156
+ sig { params(version: T.nilable(String)).returns(ExecResult) }
157
+ def bundle_install!(version: nil)
158
+ bundle("install", version: version)
159
+ end
160
+
161
+ # Run a command `bundle exec` in this context directory
162
+ sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
163
+ def bundle_exec(command, version: nil)
164
+ bundle("exec #{command}", version: version)
165
+ end
166
+
167
+ # Git
168
+
169
+ # Run a command prefixed by `git` in this context directory
170
+ sig { params(command: String).returns(ExecResult) }
171
+ def git(command)
172
+ exec("git #{command}")
173
+ end
174
+
175
+ # Run `git init` in this context directory
176
+ sig { params(branch: String).void }
177
+ def git_init!(branch: "main")
178
+ git("init -q -b #{branch}")
179
+ end
180
+
181
+ # Run `git checkout` in this context directory
182
+ sig { params(ref: String).returns(ExecResult) }
183
+ def git_checkout!(ref: "main")
184
+ git("checkout #{ref}")
185
+ end
186
+
187
+ # Get the current git branch in this context directory
188
+ sig { returns(T.nilable(String)) }
189
+ def git_current_branch
190
+ Spoom::Git.current_branch(path: @absolute_path)
191
+ end
192
+
193
+ # Sorbet
194
+
195
+ # Run `bundle exec srb` in this context directory
196
+ sig { params(command: String).returns(ExecResult) }
197
+ def srb(command)
198
+ bundle_exec("srb #{command}")
199
+ end
200
+
201
+ # Read the contents of `sorbet/config` in this context directory
202
+ sig { returns(String) }
203
+ def read_sorbet_config
204
+ read(Spoom::Sorbet::CONFIG_PATH)
205
+ end
206
+
207
+ # Set the `contents` of `sorbet/config` in this context directory
208
+ sig { params(contents: String, append: T::Boolean).void }
209
+ def write_sorbet_config!(contents, append: false)
210
+ write!(Spoom::Sorbet::CONFIG_PATH, contents, append: append)
211
+ end
212
+
213
+ # Read the strictness sigil from the file at `relative_path` (returns `nil` if no sigil)
214
+ sig { params(relative_path: String).returns(T.nilable(String)) }
215
+ def read_file_strictness(relative_path)
216
+ Spoom::Sorbet::Sigils.file_strictness(absolute_path_to(relative_path))
217
+ end
218
+ end
219
+ end
@@ -19,14 +19,18 @@ module Spoom
19
19
  @data = data
20
20
  end
21
21
 
22
- sig { returns(String) }
23
- def self.header_style
24
- ""
25
- end
26
-
27
- sig { returns(String) }
28
- def self.header_script
29
- ""
22
+ class << self
23
+ extend T::Sig
24
+
25
+ sig { returns(String) }
26
+ def header_style
27
+ ""
28
+ end
29
+
30
+ sig { returns(String) }
31
+ def header_script
32
+ ""
33
+ end
30
34
  end
31
35
 
32
36
  sig { returns(String) }
@@ -7,50 +7,52 @@ module Spoom
7
7
  module Coverage
8
8
  module D3
9
9
  class CircleMap < Base
10
- extend T::Sig
10
+ class << self
11
+ extend T::Sig
11
12
 
12
- sig { returns(String) }
13
- def self.header_style
14
- <<~CSS
15
- .node {
16
- cursor: pointer;
17
- }
13
+ sig { returns(String) }
14
+ def header_style
15
+ <<~CSS
16
+ .node {
17
+ cursor: pointer;
18
+ }
18
19
 
19
- .node:hover {
20
- stroke: #333;
21
- stroke-width: 1px;
22
- }
20
+ .node:hover {
21
+ stroke: #333;
22
+ stroke-width: 1px;
23
+ }
23
24
 
24
- .label.dir {
25
- fill: #333;
26
- }
25
+ .label.dir {
26
+ fill: #333;
27
+ }
27
28
 
28
- .label.file {
29
- font: 12px Arial, sans-serif;
30
- }
29
+ .label.file {
30
+ font: 12px Arial, sans-serif;
31
+ }
31
32
 
32
- .node.root, .node.file {
33
- pointer-events: none;
34
- }
35
- CSS
36
- end
33
+ .node.root, .node.file {
34
+ pointer-events: none;
35
+ }
36
+ CSS
37
+ end
37
38
 
38
- sig { returns(String) }
39
- def self.header_script
40
- <<~JS
41
- function treeHeight(root, height = 0) {
42
- height += 1;
43
- if (root.children && root.children.length > 0)
44
- return Math.max(...root.children.map(child => treeHeight(child, height)));
45
- else
46
- return height;
47
- }
39
+ sig { returns(String) }
40
+ def header_script
41
+ <<~JS
42
+ function treeHeight(root, height = 0) {
43
+ height += 1;
44
+ if (root.children && root.children.length > 0)
45
+ return Math.max(...root.children.map(child => treeHeight(child, height)));
46
+ else
47
+ return height;
48
+ }
48
49
 
49
- function tooltipMap(d) {
50
- moveTooltip(d)
51
- .html("<b>" + d.data.name + "</b>")
52
- }
53
- JS
50
+ function tooltipMap(d) {
51
+ moveTooltip(d)
52
+ .html("<b>" + d.data.name + "</b>")
53
+ }
54
+ JS
55
+ end
54
56
  end
55
57
 
56
58
  sig { override.returns(String) }
@@ -159,6 +161,7 @@ module Spoom
159
161
  if node.children.empty?
160
162
  return { name: node.name, strictness: tree_node_strictness(node) }
161
163
  end
164
+
162
165
  {
163
166
  name: node.name,
164
167
  children: node.children.values.map { |n| tree_node_to_json(n) },