steep 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/bin/output_rebaseline.rb +15 -30
  4. data/bin/output_test.rb +17 -57
  5. data/lib/steep.rb +4 -0
  6. data/lib/steep/cli.rb +9 -3
  7. data/lib/steep/drivers/check.rb +106 -14
  8. data/lib/steep/drivers/diagnostic_printer.rb +11 -11
  9. data/lib/steep/expectations.rb +159 -0
  10. data/lib/steep/project.rb +6 -0
  11. data/lib/steep/version.rb +1 -1
  12. data/smoke/alias/test_expectations.yml +96 -0
  13. data/smoke/and/test_expectations.yml +31 -0
  14. data/smoke/array/test_expectations.yml +103 -0
  15. data/smoke/block/test_expectations.yml +125 -0
  16. data/smoke/case/test_expectations.yml +47 -0
  17. data/smoke/class/test_expectations.yml +120 -0
  18. data/smoke/const/test_expectations.yml +139 -0
  19. data/smoke/diagnostics-rbs-duplicated/test_expectations.yml +13 -0
  20. data/smoke/diagnostics-rbs/test_expectations.yml +229 -0
  21. data/smoke/diagnostics/test_expectations.yml +477 -0
  22. data/smoke/dstr/test_expectations.yml +13 -0
  23. data/smoke/ensure/test_expectations.yml +62 -0
  24. data/smoke/enumerator/test_expectations.yml +135 -0
  25. data/smoke/extension/test_expectations.yml +61 -0
  26. data/smoke/hash/test_expectations.yml +81 -0
  27. data/smoke/hello/test_expectations.yml +25 -0
  28. data/smoke/if/test_expectations.yml +34 -0
  29. data/smoke/implements/test_expectations.yml +23 -0
  30. data/smoke/initialize/test_expectations.yml +1 -0
  31. data/smoke/integer/test_expectations.yml +101 -0
  32. data/smoke/interface/test_expectations.yml +23 -0
  33. data/smoke/kwbegin/test_expectations.yml +17 -0
  34. data/smoke/lambda/test_expectations.yml +39 -0
  35. data/smoke/literal/test_expectations.yml +106 -0
  36. data/smoke/map/test_expectations.yml +1 -0
  37. data/smoke/method/test_expectations.yml +90 -0
  38. data/smoke/module/test_expectations.yml +75 -0
  39. data/smoke/regexp/test_expectations.yml +615 -0
  40. data/smoke/regression/test_expectations.yml +43 -0
  41. data/smoke/rescue/test_expectations.yml +79 -0
  42. data/smoke/self/test_expectations.yml +23 -0
  43. data/smoke/skip/test_expectations.yml +23 -0
  44. data/smoke/stdout/test_expectations.yml +1 -0
  45. data/smoke/super/test_expectations.yml +79 -0
  46. data/smoke/toplevel/test_expectations.yml +15 -0
  47. data/smoke/tsort/test_expectations.yml +43 -0
  48. data/smoke/type_case/test_expectations.yml +48 -0
  49. data/smoke/yield/test_expectations.yml +68 -0
  50. metadata +41 -44
  51. data/smoke/alias/test.yaml +0 -73
  52. data/smoke/and/test.yaml +0 -24
  53. data/smoke/array/test.yaml +0 -80
  54. data/smoke/block/test.yaml +0 -96
  55. data/smoke/broken/Steepfile +0 -5
  56. data/smoke/broken/broken.rb +0 -0
  57. data/smoke/broken/broken.rbs +0 -0
  58. data/smoke/broken/test.yaml +0 -6
  59. data/smoke/case/test.yaml +0 -36
  60. data/smoke/class/test.yaml +0 -89
  61. data/smoke/const/test.yaml +0 -96
  62. data/smoke/diagnostics-rbs-duplicated/test.yaml +0 -10
  63. data/smoke/diagnostics-rbs/test.yaml +0 -142
  64. data/smoke/diagnostics/test.yaml +0 -333
  65. data/smoke/dstr/test.yaml +0 -10
  66. data/smoke/ensure/test.yaml +0 -47
  67. data/smoke/enumerator/test.yaml +0 -100
  68. data/smoke/extension/test.yaml +0 -50
  69. data/smoke/hash/test.yaml +0 -62
  70. data/smoke/hello/test.yaml +0 -18
  71. data/smoke/if/test.yaml +0 -27
  72. data/smoke/implements/test.yaml +0 -16
  73. data/smoke/initialize/test.yaml +0 -4
  74. data/smoke/integer/test.yaml +0 -66
  75. data/smoke/interface/test.yaml +0 -16
  76. data/smoke/kwbegin/test.yaml +0 -14
  77. data/smoke/lambda/test.yaml +0 -28
  78. data/smoke/literal/test.yaml +0 -79
  79. data/smoke/map/test.yaml +0 -4
  80. data/smoke/method/test.yaml +0 -71
  81. data/smoke/module/test.yaml +0 -51
  82. data/smoke/regexp/test.yaml +0 -372
  83. data/smoke/regression/test.yaml +0 -38
  84. data/smoke/rescue/test.yaml +0 -60
  85. data/smoke/self/test.yaml +0 -16
  86. data/smoke/skip/test.yaml +0 -16
  87. data/smoke/stdout/test.yaml +0 -4
  88. data/smoke/super/test.yaml +0 -52
  89. data/smoke/toplevel/test.yaml +0 -12
  90. data/smoke/tsort/test.yaml +0 -32
  91. data/smoke/type_case/test.yaml +0 -33
  92. data/smoke/yield/test.yaml +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725a6432404ca94fa643ba89ed2028d7b3923ece4031512ebea88ebbdc4fd01e
4
- data.tar.gz: 957a95acbda38474f1a796f829f8cf098b69484a8ddf42bd39e2c2be935a961e
3
+ metadata.gz: 7fe01659cf0cfc1c7063fede02ec238b5772bb6c6fdf44cf501ca555c719c47c
4
+ data.tar.gz: d74ebe84a911514eef46cd92a14dfedc0b90e491aa9bf284f82325a9c4a9c2b4
5
5
  SHA512:
6
- metadata.gz: 363a04610a81d4652541c9e5856705f7317a8b4e92a2074dc460b5f34e19f8eb5bfd080826c3a61a41414668d22d0352e4b300d1ce09c659243ef8c06492fc22
7
- data.tar.gz: 33b8d47d5290acea68f1f07ee74a6129ad0a2568fff2ddc783b7910f2ed55ae2d6d6b6f0b1b90b8dea773bfab0c9cc7ccc70723ab5aee75db3e3a0f997b93fe9
6
+ metadata.gz: cd2459353ab367c913f0308b1cb0dd95d297cce18ea3bb4ff537612edc0b8adc2ef4530d4bf8203c6c777fe57b8bccfbf8a8714ebc0ede67400a030e6ea1348f
7
+ data.tar.gz: b3ee38f5e340f05420783dc3f8a31ac662f475ec7bcef82222e5f07360faac1235249fa903c721a1d322b7999034b74e7f812fa47c3d3dec8a9f5763fa2693fb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.41.0 (2021-02-07)
6
+
7
+ * Add `--with-expectations` and `--save-expectations` option ([#303](https://github.com/soutaro/steep/pull/303))
8
+
5
9
  ## 0.40.0 (2021-01-31)
6
10
 
7
11
  * Report progress with dots ([#287](https://github.com/soutaro/steep/pull/287))
@@ -9,41 +9,26 @@ else
9
9
  test_dirs = ARGV.map {|p| Pathname.pwd + p }
10
10
  end
11
11
 
12
- test_dirs.each do |dir|
13
- test = dir + "test.yaml"
14
-
15
- if test.file?
16
- content = YAML.load_file(test)
17
- else
18
- content = { "test" => {} }
19
- end
12
+ failed_tests = []
20
13
 
14
+ test_dirs.each do |dir|
21
15
  puts "Rebaselining #{dir}..."
22
16
 
23
- command = content["command"] || "steep check"
24
- puts " command: #{command}"
25
-
26
- output, _ = Open3.capture2(command, chdir: dir.to_s)
17
+ command = %w(steep check --save-expectations=test_expectations.yml)
18
+ puts " command: #{command.join(" ")}"
27
19
 
28
- diagnostics = output.split(/\n\n/).each.with_object({}) do |message, hash|
29
- if message =~ /\A([^:]+):\d+:\d+:/
30
- path = $1
31
- hash[path] ||= { "diagnostics" => [] }
32
- hash[path]["diagnostics"] << message.chomp + "\n"
33
- end
34
- end
20
+ output, status = Open3.capture2(*command, chdir: dir.to_s)
35
21
 
36
- content["test"].each_key do |path|
37
- unless diagnostics.key?(path)
38
- diagnostics[path] = { "diagnostics" => [] }
39
- end
40
- end
41
-
42
- content["test"] = diagnostics.keys.sort.each.with_object({}) do |key, hash|
43
- hash[key] = diagnostics[key]
22
+ unless status.success?
23
+ puts "Error!!! 👺"
24
+ failed_tests << dir.basename
44
25
  end
26
+ end
45
27
 
46
- test.open("w") do |io|
47
- YAML.dump(content, io, header: false)
48
- end
28
+ if failed_tests.empty?
29
+ puts "Successfully updated output expectations! 🤡"
30
+ else
31
+ puts "Failed to update the following tests! 💀"
32
+ puts " #{failed_tests.join(", ")}"
33
+ exit 1
49
34
  end
data/bin/output_test.rb CHANGED
@@ -14,21 +14,27 @@ else
14
14
  test_dirs = ARGV.map {|p| Pathname.pwd + p }
15
15
  end
16
16
 
17
- success = true
17
+ failed_tests = []
18
18
 
19
19
  test_dirs.each do |dir|
20
- test = dir + "test.yaml"
21
-
22
- next unless test.file?
23
-
24
20
  puts "Running test #{dir}..."
25
21
 
26
- content = YAML.load_file(test)
22
+ unless (dir + "test_expectations.yml").file?
23
+ puts "Skipped ⛹️‍♀️"
24
+ next
25
+ end
27
26
 
28
- command = content["command"] || "steep check"
29
- puts " command: #{command}"
27
+ command = %w(steep check --with-expectations=test_expectations.yml)
28
+ puts " command: #{command.join(" ")}"
30
29
 
31
- output, _ = Open3.capture2(command, chdir: dir.to_s)
30
+ output, status = Open3.capture2(*command, chdir: dir.to_s)
31
+
32
+ unless status.success?
33
+ failed_tests << dir.basename
34
+ puts " Failed! 🤕"
35
+ else
36
+ puts " Succeed! 👍"
37
+ end
32
38
 
33
39
  if @verbose
34
40
  puts " Raw output:"
@@ -36,58 +42,12 @@ test_dirs.each do |dir|
36
42
  puts " > #{line.chomp}"
37
43
  end
38
44
  end
39
-
40
- diagnostics = output.split(/\n\n/).each.with_object({}) do |d, hash|
41
- if d =~ /\A([^:]+):\d+:\d+:/
42
- path = $1
43
- hash[path] ||= []
44
- hash[path] << (d.chomp + "\n")
45
- end
46
- end
47
-
48
- content["test"].each do |path, test|
49
- puts " Checking: #{path}..."
50
-
51
- fail_expected = test["fail"] || false
52
-
53
- expected_diagnostics = test["diagnostics"]
54
- reported_diagnostics = (diagnostics[path] || [])
55
-
56
- puts " # of expected: #{expected_diagnostics.size}, # of reported: #{reported_diagnostics.size}"
57
-
58
- unexpected_diagnostics = reported_diagnostics.reject {|d| expected_diagnostics.include?(d) }
59
- missing_diagnostics = expected_diagnostics.reject {|d| reported_diagnostics.include?(d) }
60
-
61
- unexpected_diagnostics.each do |d|
62
- puts " Unexpected diagnostics:"
63
- d.split(/\n/).each do |line|
64
- puts " + #{line.chomp}"
65
- end
66
- end
67
-
68
- missing_diagnostics.each do |d|
69
- puts " Missing diagnostics:"
70
- d.split(/\n/).each do |line|
71
- puts " - #{line.chomp}"
72
- end
73
- end
74
-
75
- if unexpected_diagnostics.empty? && missing_diagnostics.empty?
76
- puts " 👍"
77
- else
78
- if fail_expected
79
- puts " 🚨 (expected failure)"
80
- else
81
- puts " 🚨"
82
- success = false
83
- end
84
- end
85
- end
86
45
  end
87
46
 
88
- if success
47
+ if failed_tests.empty?
89
48
  puts "All tests ok! 👏"
90
49
  else
91
50
  puts "Errors detected! 🤮"
51
+ puts " #{failed_tests.join(", ")}"
92
52
  exit 1
93
53
  end
data/lib/steep.rb CHANGED
@@ -4,6 +4,7 @@ require "pathname"
4
4
  require "parser/ruby27"
5
5
  require "ast_utils"
6
6
  require "active_support/core_ext/object/try"
7
+ require "active_support/core_ext/string/inflections"
7
8
  require "logger"
8
9
  require "active_support/tagged_logging"
9
10
  require "rainbow"
@@ -13,6 +14,7 @@ require "etc"
13
14
  require "open3"
14
15
  require "stringio"
15
16
  require 'uri'
17
+ require "yaml"
16
18
 
17
19
  require "rbs"
18
20
 
@@ -102,6 +104,8 @@ require "steep/project/file_loader"
102
104
  require "steep/project/hover_content"
103
105
  require "steep/project/completion_provider"
104
106
  require "steep/project/stats_calculator"
107
+
108
+ require "steep/expectations"
105
109
  require "steep/drivers/utils/driver_helper"
106
110
  require "steep/drivers/check"
107
111
  require "steep/drivers/stats"
data/lib/steep/cli.rb CHANGED
@@ -51,15 +51,15 @@ module Steep
51
51
  end
52
52
 
53
53
  def handle_logging_options(opts)
54
- opts.on("--log-level=[debug,info,warn,error,fatal]") do |level|
54
+ opts.on("--log-level=LEVEL", "Specify log level: debug, info, warn, error, fatal") do |level|
55
55
  Steep.logger.level = level
56
56
  end
57
57
 
58
- opts.on("--log-output=[PATH]") do |file|
58
+ opts.on("--log-output=PATH", "Print logs to given path") do |file|
59
59
  Steep.log_output = file
60
60
  end
61
61
 
62
- opts.on("--verbose") do
62
+ opts.on("--verbose", "Set log level to debug") do
63
63
  Steep.logger.level = Logger::DEBUG
64
64
  end
65
65
  end
@@ -83,6 +83,12 @@ module Steep
83
83
  opts.banner = "Usage: steep check [options] [sources]"
84
84
 
85
85
  opts.on("--steepfile=PATH") {|path| check.steepfile = Pathname(path) }
86
+ opts.on("--with-expectations[=PATH]", "Type check with expectations saved in PATH (or steep_expectations.yml)") do |path|
87
+ check.with_expectations_path = Pathname(path || "steep_expectations.yml")
88
+ end
89
+ opts.on("--save-expectations[=PATH]", "Save expectations with current type check result to PATH (or steep_expectations.yml)") do |path|
90
+ check.save_expectations_path = Pathname(path || "steep_expectations.yml")
91
+ end
86
92
  handle_logging_options opts
87
93
  end.parse!(argv)
88
94
 
@@ -6,6 +6,8 @@ module Steep
6
6
  attr_reader :stdout
7
7
  attr_reader :stderr
8
8
  attr_reader :command_line_patterns
9
+ attr_accessor :with_expectations_path
10
+ attr_accessor :save_expectations_path
9
11
 
10
12
  include Utils::DriverHelper
11
13
 
@@ -57,7 +59,7 @@ module Steep
57
59
  shutdown_id = -1
58
60
  client_writer.write({ method: :shutdown, id: shutdown_id })
59
61
 
60
- responses = []
62
+ diagnostic_notifications = []
61
63
  error_messages = []
62
64
  client_reader.read do |response|
63
65
  case
@@ -68,7 +70,7 @@ module Steep
68
70
  else
69
71
  stdout.print "F"
70
72
  end
71
- responses << response[:params]
73
+ diagnostic_notifications << response[:params]
72
74
  stdout.flush
73
75
  when response[:method] == "window/showMessage"
74
76
  # Assuming ERROR message means unrecoverable error.
@@ -89,30 +91,120 @@ module Steep
89
91
  stdout.puts
90
92
  stdout.puts
91
93
 
92
- case
93
- when responses.all? {|res| res[:diagnostics].empty? } && error_messages.empty?
94
+ if error_messages.empty?
95
+ case
96
+ when with_expectations_path
97
+ print_expectations(project: project,
98
+ expectations_path: with_expectations_path,
99
+ notifications: diagnostic_notifications)
100
+ when save_expectations_path
101
+ save_expectations(project: project,
102
+ expectations_path: save_expectations_path,
103
+ notifications: diagnostic_notifications)
104
+ else
105
+ print_result(project: project, notifications: diagnostic_notifications)
106
+ end
107
+ else
108
+ stdout.puts Rainbow("Unexpected error reported. 🚨").red.bold
109
+ 1
110
+ end
111
+ end
112
+
113
+ def print_expectations(project:, expectations_path:, notifications:)
114
+ expectations = Expectations.load(path: expectations_path, content: expectations_path.read)
115
+
116
+ expected_count = 0
117
+ unexpected_count = 0
118
+ missing_count = 0
119
+
120
+ ns = notifications.each.with_object({}) do |notification, hash|
121
+ path = project.relative_path(Pathname(URI.parse(notification[:uri]).path))
122
+ hash[path] = notification[:diagnostics]
123
+ end
124
+
125
+ (project.all_source_files + project.all_signature_files).sort.each do |path|
126
+ test = expectations.test(path: path, diagnostics: ns[path] || [])
127
+
128
+ buffer = RBS::Buffer.new(name: path, content: path.read)
129
+ printer = DiagnosticPrinter.new(buffer: buffer, stdout: stdout)
130
+
131
+ test.each_diagnostics.each do |type, diag|
132
+ case type
133
+ when :expected
134
+ expected_count += 1
135
+ when :unexpected
136
+ unexpected_count += 1
137
+ printer.print(diag, prefix: Rainbow("+ ").green)
138
+ when :missing
139
+ missing_count += 1
140
+ printer.print(diag, prefix: Rainbow("- ").red)
141
+ end
142
+ end
143
+ end
144
+
145
+ if unexpected_count > 0 || missing_count > 0
146
+ stdout.puts
147
+
148
+ stdout.puts Rainbow("Expectations unsatisfied:").bold.red
149
+ stdout.puts " #{expected_count} expected #{"diagnostic".pluralize(expected_count)}"
150
+ stdout.puts Rainbow(" + #{unexpected_count} unexpected #{"diagnostic".pluralize(unexpected_count)}").green
151
+ stdout.puts Rainbow(" - #{missing_count} missing #{"diagnostic".pluralize(missing_count)}").red
152
+ 1
153
+ else
154
+ stdout.puts Rainbow("Expectations satisfied:").bold.green
155
+ stdout.puts " #{expected_count} expected #{"diagnostic".pluralize(expected_count)}"
156
+ 0
157
+ end
158
+ end
159
+
160
+ def save_expectations(project:, expectations_path:, notifications:)
161
+ expectations = if expectations_path.file?
162
+ Expectations.load(path: expectations_path, content: expectations_path.read)
163
+ else
164
+ Expectations.empty()
165
+ end
166
+
167
+ ns = notifications.each.with_object({}) do |notification, hash|
168
+ path = project.relative_path(Pathname(URI.parse(notification[:uri]).path))
169
+ hash[path] = notification[:diagnostics]
170
+ end
171
+
172
+ (project.all_source_files + project.all_signature_files).sort.each do |path|
173
+ ds = ns[path] || []
174
+
175
+ if ds.empty?
176
+ expectations.diagnostics.delete(path)
177
+ else
178
+ expectations.diagnostics[path] = ds
179
+ end
180
+ end
181
+
182
+ expectations_path.write(expectations.to_yaml)
183
+ stdout.puts Rainbow("Saved expectations in #{expectations_path}...").bold
184
+ 0
185
+ end
186
+
187
+ def print_result(project:, notifications:)
188
+ if notifications.all? {|notification| notification[:diagnostics].empty? }
94
189
  emoji = %w(🫖 🫖 🫖 🫖 🫖 🫖 🫖 🫖 🍵 🧋 🧉).sample
95
190
  stdout.puts Rainbow("No type error detected. #{emoji}").green.bold
96
191
  0
97
- when !error_messages.empty?
98
- stdout.puts Rainbow("Unexpected error reported. 🚨").red.bold
99
- 1
100
192
  else
101
- errors = responses.reject {|res| res[:diagnostics].empty? }
102
- total = errors.sum {|res| res[:diagnostics].size }
103
- stdout.puts Rainbow("Detected #{total} problems from #{errors.size} files").red.bold
104
- stdout.puts
193
+ errors = notifications.reject {|notification| notification[:diagnostics].empty? }
194
+ total = errors.sum {|notification| notification[:diagnostics].size }
105
195
 
106
- errors.each do |resp|
107
- path = project.relative_path(Pathname(URI.parse(resp[:uri]).path))
196
+ errors.each do |notification|
197
+ path = project.relative_path(Pathname(URI.parse(notification[:uri]).path))
108
198
  buffer = RBS::Buffer.new(name: path, content: path.read)
109
199
  printer = DiagnosticPrinter.new(buffer: buffer, stdout: stdout)
110
200
 
111
- resp[:diagnostics].each do |diag|
201
+ notification[:diagnostics].each do |diag|
112
202
  printer.print(diag)
113
203
  stdout.puts
114
204
  end
115
205
  end
206
+
207
+ stdout.puts Rainbow("Detected #{total} #{"problem".pluralize(total)} from #{errors.size} #{"file".pluralize(errors.size)}").red.bold
116
208
  1
117
209
  end
118
210
  end
@@ -50,28 +50,28 @@ module Steep
50
50
  Rainbow("#{path}:#{start[:line]+1}:#{start[:character]}").magenta
51
51
  end
52
52
 
53
- def print(diagnostic)
53
+ def print(diagnostic, prefix: "")
54
54
  header, *rest = diagnostic[:message].split(/\n/)
55
55
 
56
- stdout.puts "#{location(diagnostic)}: [#{severity_message(diagnostic[:severity])}] #{Rainbow(header).underline}"
56
+ stdout.puts "#{prefix}#{location(diagnostic)}: [#{severity_message(diagnostic[:severity])}] #{Rainbow(header).underline}"
57
57
 
58
58
  unless rest.empty?
59
59
  rest.each do |message|
60
- stdout.puts "│ #{message}"
60
+ stdout.puts "#{prefix}│ #{message}"
61
61
  end
62
62
  end
63
63
 
64
64
  if diagnostic[:code]
65
- stdout.puts "│" unless rest.empty?
66
- stdout.puts "│ Diagnostic ID: #{diagnostic[:code]}"
65
+ stdout.puts "#{prefix}│" unless rest.empty?
66
+ stdout.puts "#{prefix}│ Diagnostic ID: #{diagnostic[:code]}"
67
67
  end
68
68
 
69
- stdout.puts "│"
69
+ stdout.puts "#{prefix}│"
70
70
 
71
- print_source_line(diagnostic)
71
+ print_source_line(diagnostic, prefix: prefix)
72
72
  end
73
73
 
74
- def print_source_line(diagnostic)
74
+ def print_source_line(diagnostic, prefix: "")
75
75
  start_pos = diagnostic[:range][:start]
76
76
  end_pos = diagnostic[:range][:end]
77
77
 
@@ -80,14 +80,14 @@ module Steep
80
80
  leading = line[0...start_pos[:character]]
81
81
  if start_pos[:line] == end_pos[:line]
82
82
  subject = line[start_pos[:character]...end_pos[:character]]
83
- trailing = line[end_pos[:character]...].chomp
83
+ trailing = (line[end_pos[:character]...] || "").chomp
84
84
  else
85
85
  subject = line[start_pos[:character]...].chomp
86
86
  trailing = ""
87
87
  end
88
88
 
89
- stdout.puts "└ #{leading}#{color_severity(subject, severity: diagnostic[:severity])}#{trailing}"
90
- stdout.puts " #{" " * leading.size}#{"~" * subject.size}"
89
+ stdout.puts "#{prefix}└ #{leading}#{color_severity(subject, severity: diagnostic[:severity])}#{trailing}"
90
+ stdout.puts "#{prefix} #{" " * leading.size}#{"~" * subject.size}"
91
91
  end
92
92
  end
93
93
  end