codeclimate 0.29.0 → 0.30.0

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
  SHA1:
3
- metadata.gz: efa08f642f16f2216254aad188d1e577569710cd
4
- data.tar.gz: cab24ce9e68f7cb1622d0549dc3608b8901b86c3
3
+ metadata.gz: a3f32cefb371cbbde1e958f6e6249101cd6aaba0
4
+ data.tar.gz: 63bf94a4ec496821d32a185247ab24a5b52502ba
5
5
  SHA512:
6
- metadata.gz: c8e00338ff2aed40e05240529f434a6a01f6c57a0bc2e4230079d34a653da771bf9cf3fd09abe6aec7115085da49304d813dd0460903a9141a1d1abbce21c81d
7
- data.tar.gz: 9f91b473418c645f5bcf1c78cd6cfd2e8f1e603165b5d9d97dbab3d974880d423b6eec6b86b1e6f22a9792933d3f30ee3ee344cd05e9d104ee7f6e2ca168716e
6
+ metadata.gz: b847ba8b39c19a3e5f17254271aeee4641cbc0bf95ff8c383a10ff24f354c40ad21e57de65093953cb8d334c528214c2840744d6a66018a62b1ffa8ed80c6754
7
+ data.tar.gz: d912d8ead585a16848ea2f06b35cc5dafdf3853fdcad8ab45aacb8db7de9446ec9e86e5de7b806d10c9bf2bafc008de8655965b01a5da8c7199d9a516fbc6825
@@ -1,8 +1,8 @@
1
1
  # This file lists all the engines available to be run for analysis.
2
2
  #
3
- # Each engine must have an `image` and `description`. The value in `image` will
4
- # be passed to `docker run` and so may be any value appropriate for that
5
- # (repo/name:tag, image id, etc).
3
+ # Each engine must have `channels` (with a `stable` key) and `description`. The
4
+ # values in `channels` will be passed to `docker run` and so may be any value
5
+ # appropriate for that (repo/name:tag, image id, etc).
6
6
  #
7
7
  # When a repo has files that match the `enable_regexps`, that engine will be
8
8
  # enabled by default in the codeclimate.yml file. That file will also have in it
@@ -10,7 +10,8 @@
10
10
  # which files should be rated.
11
11
  #
12
12
  brakeman:
13
- image: codeclimate/codeclimate-brakeman
13
+ channels:
14
+ stable: codeclimate/codeclimate-brakeman
14
15
  description: Static analysis tool which checks Ruby on Rails applications for security vulnerabilities.
15
16
  community: false
16
17
  upgrade_languages:
@@ -25,7 +26,8 @@ brakeman:
25
26
  - "**.rhtml"
26
27
  - "**.slim"
27
28
  bundler-audit:
28
- image: codeclimate/codeclimate-bundler-audit
29
+ channels:
30
+ stable: codeclimate/codeclimate-bundler-audit
29
31
  description: Patch-level verification for Bundler.
30
32
  community: false
31
33
  upgrade_languages:
@@ -35,7 +37,8 @@ bundler-audit:
35
37
  default_ratings_paths:
36
38
  - Gemfile.lock
37
39
  csslint:
38
- image: codeclimate/codeclimate-csslint
40
+ channels:
41
+ stable: codeclimate/codeclimate-csslint
39
42
  description: Automated linting of Cascading Stylesheets.
40
43
  community: false
41
44
  enable_regexps:
@@ -43,7 +46,8 @@ csslint:
43
46
  default_ratings_paths:
44
47
  - "**.css"
45
48
  coffeelint:
46
- image: codeclimate/codeclimate-coffeelint
49
+ channels:
50
+ stable: codeclimate/codeclimate-coffeelint
47
51
  description: A style checker for CoffeeScript.
48
52
  community: false
49
53
  enable_regexps:
@@ -51,7 +55,8 @@ coffeelint:
51
55
  default_ratings_paths:
52
56
  - "**.coffee"
53
57
  duplication:
54
- image: codeclimate/codeclimate-duplication
58
+ channels:
59
+ stable: codeclimate/codeclimate-duplication
55
60
  description: Structural duplication detection for Ruby, Python, JavaScript, and PHP.
56
61
  community: false
57
62
  enable_regexps:
@@ -77,7 +82,8 @@ duplication:
77
82
  - python
78
83
  - php
79
84
  eslint:
80
- image: codeclimate/codeclimate-eslint
85
+ channels:
86
+ stable: codeclimate/codeclimate-eslint
81
87
  description: A JavaScript/JSX linting utility.
82
88
  community: false
83
89
  upgrade_languages:
@@ -89,7 +95,8 @@ eslint:
89
95
  - "**.js"
90
96
  - "**.jsx"
91
97
  gofmt:
92
- image: codeclimate/codeclimate-gofmt
98
+ channels:
99
+ stable: codeclimate/codeclimate-gofmt
93
100
  description: Checks the formatting of Go programs.
94
101
  community: true
95
102
  enable_regexps:
@@ -97,7 +104,8 @@ gofmt:
97
104
  default_ratings_paths:
98
105
  - "**.go"
99
106
  golint:
100
- image: codeclimate/codeclimate-golint
107
+ channels:
108
+ stable: codeclimate/codeclimate-golint
101
109
  description: A linter for Go.
102
110
  community: true
103
111
  enable_regexps:
@@ -105,7 +113,8 @@ golint:
105
113
  default_ratings_paths:
106
114
  - "**.go"
107
115
  govet:
108
- image: codeclimate/codeclimate-govet
116
+ channels:
117
+ stable: codeclimate/codeclimate-govet
109
118
  description: Reports suspicious constructs in Go programs.
110
119
  community: true
111
120
  enable_regexps:
@@ -113,20 +122,23 @@ govet:
113
122
  default_ratings_paths:
114
123
  - "**.go"
115
124
  fixme:
116
- image: codeclimate/codeclimate-fixme
125
+ channels:
126
+ stable: codeclimate/codeclimate-fixme
117
127
  description: Finds FIXME, TODO, HACK, etc. comments.
118
128
  community: false
119
129
  enable_regexps:
120
130
  - .+
121
131
  default_ratings_paths: []
122
132
  foodcritic:
123
- image: codeclimate/codeclimate-foodcritic
133
+ channels:
134
+ stable: codeclimate/codeclimate-foodcritic
124
135
  description: Lint tool for Chef cookbooks.
125
136
  community: true
126
137
  enable_regexps:
127
138
  default_ratings_paths:
128
139
  gnu-complexity:
129
- image: codeclimate/codeclimate-gnu-complexity
140
+ channels:
141
+ stable: codeclimate/codeclimate-gnu-complexity
130
142
  description: Checks complexity of C code
131
143
  community: true
132
144
  enable_regexps:
@@ -134,7 +146,8 @@ gnu-complexity:
134
146
  default_ratings_paths:
135
147
  - "**.c"
136
148
  haxe-checkstyle:
137
- image: codeclimate/codeclimate-haxe-checkstyle
149
+ channels:
150
+ stable: codeclimate/codeclimate-haxe-checkstyle
138
151
  description: Checkstyle is a development library to help developers write Haxe code that adheres to a coding standard.
139
152
  community: true
140
153
  enable_regexps:
@@ -142,7 +155,8 @@ haxe-checkstyle:
142
155
  default_ratings_paths:
143
156
  - "**.hx"
144
157
  hlint:
145
- image: codeclimate/codeclimate-hlint
158
+ channels:
159
+ stable: codeclimate/codeclimate-hlint
146
160
  description: Linter for Haskell programs.
147
161
  community: true
148
162
  enable_regexps:
@@ -150,7 +164,8 @@ hlint:
150
164
  default_ratings_paths:
151
165
  - "**.hs"
152
166
  kibit:
153
- image: codeclimate/codeclimate-kibit
167
+ channels:
168
+ stable: codeclimate/codeclimate-kibit
154
169
  description: Static code analyzer for Clojure, ClojureScript, cljx and other Clojure variants.
155
170
  community: true
156
171
  enable_regexps:
@@ -162,7 +177,8 @@ kibit:
162
177
  - "**.cljc"
163
178
  - "**.cljs"
164
179
  markdownlint:
165
- image: codeclimate/codeclimate-markdownlint
180
+ channels:
181
+ stable: codeclimate/codeclimate-markdownlint
166
182
  description: Flags style issues within Markdown files.
167
183
  community: true
168
184
  enable_regexps:
@@ -172,20 +188,23 @@ markdownlint:
172
188
  - "**.markdown"
173
189
  - "**.md"
174
190
  nodesecurity:
175
- image: codeclimate/codeclimate-nodesecurity
191
+ channels:
192
+ stable: codeclimate/codeclimate-nodesecurity
176
193
  description: Security tool for Node.js dependencies.
177
194
  community: true
178
195
  enable_regexps:
179
196
  default_ratings_paths:
180
197
  pep8:
181
- image: codeclimate/codeclimate-pep8
198
+ channels:
199
+ stable: codeclimate/codeclimate-pep8
182
200
  description: Static analysis tool to check Python code against the style conventions outlined in PEP-8.
183
201
  community: false
184
202
  enable_regexps:
185
203
  default_ratings_paths:
186
204
  - "**.py"
187
205
  phan:
188
- image: codeclimate/codeclimate-phan
206
+ channels:
207
+ stable: codeclimate/codeclimate-phan
189
208
  description: Phan is a static analyzer for PHP.
190
209
  community: true
191
210
  enable_regexps:
@@ -197,7 +216,8 @@ phan:
197
216
  - "**.module"
198
217
  - "**.inc"
199
218
  phpcodesniffer:
200
- image: codeclimate/codeclimate-phpcodesniffer
219
+ channels:
220
+ stable: codeclimate/codeclimate-phpcodesniffer
201
221
  description: Detects violations of a defined set of coding standards in PHP.
202
222
  community: false
203
223
  enable_regexps:
@@ -206,7 +226,8 @@ phpcodesniffer:
206
226
  - "**.module"
207
227
  - "**.inc"
208
228
  phpmd:
209
- image: codeclimate/codeclimate-phpmd
229
+ channels:
230
+ stable: codeclimate/codeclimate-phpmd
210
231
  description: A PHP static analysis tool.
211
232
  community: false
212
233
  upgrade_languages:
@@ -220,7 +241,8 @@ phpmd:
220
241
  - "**.module"
221
242
  - "**.inc"
222
243
  radon:
223
- image: codeclimate/codeclimate-radon
244
+ channels:
245
+ stable: codeclimate/codeclimate-radon
224
246
  description: Python tool used to compute Cyclomatic Complexity.
225
247
  community: false
226
248
  upgrade_languages:
@@ -230,7 +252,8 @@ radon:
230
252
  default_ratings_paths:
231
253
  - "**.py"
232
254
  reek:
233
- image: codeclimate/codeclimate-reek
255
+ channels:
256
+ stable: codeclimate/codeclimate-reek
234
257
  description: "Reek examines Ruby classes, modules, and methods and reports any code smells it finds."
235
258
  community: true
236
259
  upgrade_languages:
@@ -240,13 +263,15 @@ reek:
240
263
  default_ratings_paths:
241
264
  - "**.rb"
242
265
  requiresafe:
243
- image: codeclimate/codeclimate-nodesecurity
266
+ channels:
267
+ stable: codeclimate/codeclimate-nodesecurity
244
268
  description: Security tool for Node.js dependencies.
245
269
  community: true
246
270
  enable_regexps:
247
271
  default_ratings_paths:
248
272
  rubocop:
249
- image: codeclimate/codeclimate-rubocop
273
+ channels:
274
+ stable: codeclimate/codeclimate-rubocop
250
275
  description: A Ruby static code analyzer, based on the community Ruby style guide.
251
276
  community: false
252
277
  upgrade_languages:
@@ -256,28 +281,32 @@ rubocop:
256
281
  default_ratings_paths:
257
282
  - "**.rb"
258
283
  rubocop-v35:
259
- image: codeclimate/codeclimate-rubocop:v35
284
+ channels:
285
+ stable: codeclimate/codeclimate-rubocop:v35
260
286
  description: A Ruby static code analyzer, based on the community Ruby style guide. Version 0.35.1 of RuboCop.
261
287
  community: false
262
288
  enable_regexps:
263
289
  default_ratings_paths:
264
290
  - "**.rb"
265
291
  rubymotion:
266
- image: codeclimate/codeclimate-rubymotion
292
+ channels:
293
+ stable: codeclimate/codeclimate-rubymotion
267
294
  description: Rubymotion-specific rubocop checks.
268
295
  community: true
269
296
  enable_regexps:
270
297
  default_ratings_paths:
271
298
  - "**.rb"
272
299
  scss-lint:
273
- image: codeclimate/codeclimate-scss-lint
300
+ channels:
301
+ stable: codeclimate/codeclimate-scss-lint
274
302
  description: Configurable tool for writing clean and consistent SCSS.
275
303
  community: true
276
304
  enable_regexps:
277
305
  default_ratings_paths:
278
306
  - "**.scss"
279
307
  shellcheck:
280
- image: codeclimate/codeclimate-shellcheck
308
+ channels:
309
+ stable: codeclimate/codeclimate-shellcheck
281
310
  description: A static analysis tool for shell scripts.
282
311
  community: true
283
312
  enable_regexps:
@@ -285,7 +314,8 @@ shellcheck:
285
314
  default_ratings_paths:
286
315
  - "**.sh"
287
316
  tailor:
288
- image: codeclimate/codeclimate-tailor
317
+ channels:
318
+ stable: codeclimate/codeclimate-tailor
289
319
  description: Cross-platform static analyzer and linter for Swift.
290
320
  community: true
291
321
  enable_regexps:
@@ -293,14 +323,16 @@ tailor:
293
323
  default_ratings_paths:
294
324
  - "**.swift"
295
325
  watson:
296
- image: codeclimate/codeclimate-watson
326
+ channels:
327
+ stable: codeclimate/codeclimate-watson
297
328
  description: A young Ember Doctor to help you fix your code.
298
329
  community: true
299
330
  enable_regexps:
300
331
  default_ratings_paths:
301
332
  - "app/**"
302
333
  vint:
303
- image: codeclimate/codeclimate-vint
334
+ channels:
335
+ stable: codeclimate/codeclimate-vint
304
336
  description: Fast and Highly Extensible Vim script Language Lint implemented by Python.
305
337
  community: true
306
338
  enable_regexps:
@@ -52,6 +52,11 @@ module CC
52
52
  container.run(container_options).tap do |result|
53
53
  CLI.debug("#{name} engine stderr: #{result.stderr}")
54
54
  end
55
+ rescue Container::ImageRequired
56
+ # Provide a clearer message given the context we have
57
+ message = "Unable to find an image for #{name}:#{@config["channel"]}."
58
+ message << " Available channels: #{@metadata["channels"].keys.inspect}."
59
+ raise Container::ImageRequired, message
55
60
  ensure
56
61
  delete_config_file
57
62
  end
@@ -3,6 +3,19 @@ require "securerandom"
3
3
  module CC
4
4
  module Analyzer
5
5
  class EnginesConfigBuilder
6
+ class RegistryAdapter < SimpleDelegator
7
+ # Calling this is guarded by Registry#key?(name) so we can assume
8
+ # metadata itself will be present. We own the YAML loaded into the
9
+ # registry, so we can also assume the "channels" key will be present. We
10
+ # can't assume it will have a key for the given channel, but the nil
11
+ # value for the returned image key will trigger the desired error
12
+ # handling.
13
+ def fetch(name, channel)
14
+ metadata = self[name]
15
+ metadata.merge("image" => metadata["channels"][channel])
16
+ end
17
+ end
18
+
6
19
  Result = Struct.new(
7
20
  :name,
8
21
  :registry_entry,
@@ -12,7 +25,7 @@ module CC
12
25
  )
13
26
 
14
27
  def initialize(registry:, config:, container_label:, source_dir:, requested_paths:)
15
- @registry = registry
28
+ @registry = RegistryAdapter.new(registry)
16
29
  @config = config
17
30
  @container_label = container_label
18
31
  @requested_paths = requested_paths
@@ -23,7 +36,8 @@ module CC
23
36
  names_and_raw_engine_configs.map do |name, raw_engine_config|
24
37
  label = @container_label || SecureRandom.uuid
25
38
  engine_config = engine_config(raw_engine_config)
26
- Result.new(name, @registry[name], @source_dir, engine_config, label)
39
+ engine_metadata = @registry.fetch(name, raw_engine_config.channel)
40
+ Result.new(name, engine_metadata, @source_dir, engine_config, label)
27
41
  end
28
42
  end
29
43
 
@@ -2,11 +2,13 @@ module CC
2
2
  module Analyzer
3
3
  module Formatters
4
4
  autoload :Formatter, "cc/analyzer/formatters/formatter"
5
+ autoload :HTMLFormatter, "cc/analyzer/formatters/html_formatter"
5
6
  autoload :JSONFormatter, "cc/analyzer/formatters/json_formatter"
6
7
  autoload :PlainTextFormatter, "cc/analyzer/formatters/plain_text_formatter"
7
8
  autoload :Spinner, "cc/analyzer/formatters/spinner"
8
9
 
9
10
  FORMATTERS = {
11
+ html: HTMLFormatter,
10
12
  json: JSONFormatter,
11
13
  text: PlainTextFormatter,
12
14
  }.freeze
@@ -0,0 +1,82 @@
1
+ require "redcarpet"
2
+ require "active_support/number_helper"
3
+
4
+ module CC
5
+ module Analyzer
6
+ module Formatters
7
+ class HTMLFormatter < Formatter
8
+ class ReportTemplate
9
+ include ERB::Util
10
+ attr_accessor :template, :issues, :issues_by_path
11
+
12
+ TEMPLATE_PATH = File.expand_path(File.join(File.dirname(__FILE__), "templates/html.erb"))
13
+
14
+ def initialize(issue_count, issues_by_path)
15
+ @template = File.read(TEMPLATE_PATH)
16
+ @issues_by_path = issues_by_path
17
+ @issue_count = issue_count
18
+ end
19
+
20
+ def render
21
+ ERB.new(@template).result(binding)
22
+ end
23
+
24
+ def pluralize(number, noun)
25
+ "#{ActiveSupport::NumberHelper.number_to_delimited(number)} #{noun.pluralize(number)}"
26
+ end
27
+ end
28
+
29
+ def write(data)
30
+ json = JSON.parse(data)
31
+ json["engine_name"] = current_engine.name
32
+
33
+ case json["type"].downcase
34
+ when "issue"
35
+ issues << json
36
+ when "warning"
37
+ warnings << json
38
+ else
39
+ raise "Invalid type found: #{json["type"]}"
40
+ end
41
+ end
42
+
43
+ def finished
44
+ template = ReportTemplate.new(issues.length, issues_by_path)
45
+ puts template.render
46
+ end
47
+
48
+ def failed(_)
49
+ exit 1
50
+ end
51
+
52
+ private
53
+
54
+ def issues
55
+ @issues ||= []
56
+ end
57
+
58
+ def issues_by_path
59
+ issues.group_by { |i| i["location"]["path"] }.sort.each do |_, file_issues|
60
+ IssueSorter.new(file_issues).by_location.map do |issue|
61
+ source_buffer = @filesystem.source_buffer_for(issue["location"]["path"])
62
+ issue["location"] = LocationDescription.new(source_buffer, issue["location"], "")
63
+ issue["description"] = render_readup_markdown(issue["description"])
64
+ if issue["content"]
65
+ issue["content"]["body"] = render_readup_markdown(issue["content"]["body"])
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def warnings
72
+ @warnings ||= []
73
+ end
74
+
75
+ def render_readup_markdown(body)
76
+ html = Redcarpet::Render::HTML.new(escape_html: false, link_attributes: { target: "_blank" })
77
+ Redcarpet::Markdown.new(html, autolink: true, fenced_code_blocks: true, tables: true).render(body)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -83,10 +83,15 @@ module CC
83
83
 
84
84
  def add_engine_options
85
85
  engine_options.each do |engine|
86
+ name, channel = engine.split(":", 2)
87
+
86
88
  if config.engines.include?(engine)
87
- config.engines[engine].enabled = true
89
+ config.engines[name].enabled = true
90
+ config.engines[name].channel = channel if channel
88
91
  else
89
- config.engines[engine] = CC::Yaml::Nodes::Engine.new(config.engines).with_value("enabled" => true)
92
+ value = { "enabled" => true }
93
+ value["channel"] = channel if channel
94
+ config.engines[name] = CC::Yaml::Nodes::Engine.new(config.engines).with_value(value)
90
95
  end
91
96
  end
92
97
  end
@@ -16,8 +16,8 @@ module CC
16
16
  def pull_docker_images
17
17
  engine_names.each do |name|
18
18
  if engine_registry.exists?(name)
19
- image = engine_image(name)
20
- pull_engine_image(image)
19
+ images = engine_registry[name]["channels"].values
20
+ images.each { |image| pull_engine_image(image) }
21
21
  else
22
22
  warn("unknown engine name: #{name}")
23
23
  end
@@ -28,10 +28,6 @@ module CC
28
28
  @engine_names ||= parsed_yaml.engine_names
29
29
  end
30
30
 
31
- def engine_image(engine_name)
32
- engine_registry_list[engine_name]["image"]
33
- end
34
-
35
31
  def pull_engine_image(engine_image)
36
32
  unless system("docker pull #{engine_image}")
37
33
  raise ImagePullFailure, "unable to pull image #{engine_image}"
@@ -14,7 +14,7 @@ module CC
14
14
 
15
15
  def commands
16
16
  [
17
- "analyze [-f format] [-e engine] <path>",
17
+ "analyze [-f format] [-e engine(:channel)] <path>",
18
18
  "console",
19
19
  "engines:disable #{underline("engine_name")}",
20
20
  "engines:enable #{underline("engine_name")}",
@@ -234,7 +234,7 @@ module CC
234
234
  end
235
235
 
236
236
  def engine_image
237
- engine_registry[@engine_name]["image"]
237
+ engine_registry[@engine_name]["channels"]["stable"]
238
238
  end
239
239
 
240
240
  # Stolen from ActiveSupport (where it was deprecated)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codeclimate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code Climate
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-20 00:00:00.000000000 Z
11
+ date: 2016-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -50,14 +50,14 @@ dependencies:
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 0.8.0
53
+ version: 0.9.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 0.8.0
60
+ version: 0.9.0
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: highline
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +132,20 @@ dependencies:
132
132
  - - ">="
133
133
  - !ruby/object:Gem::Version
134
134
  version: 2.0.0
135
+ - !ruby/object:Gem::Dependency
136
+ name: redcarpet
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '3.2'
142
+ type: :runtime
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: '3.2'
135
149
  description: Code Climate command line tool
136
150
  email: hello@codeclimate.com
137
151
  executables:
@@ -164,6 +178,7 @@ files:
164
178
  - lib/cc/analyzer/filesystem.rb
165
179
  - lib/cc/analyzer/formatters.rb
166
180
  - lib/cc/analyzer/formatters/formatter.rb
181
+ - lib/cc/analyzer/formatters/html_formatter.rb
167
182
  - lib/cc/analyzer/formatters/json_formatter.rb
168
183
  - lib/cc/analyzer/formatters/plain_text_formatter.rb
169
184
  - lib/cc/analyzer/formatters/spinner.rb