slim_lint_standard 0.0.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/bin/slim-lint-standard +7 -0
  4. data/config/default.yml +109 -0
  5. data/lib/slim_lint/atom.rb +129 -0
  6. data/lib/slim_lint/capture_map.rb +19 -0
  7. data/lib/slim_lint/cli.rb +167 -0
  8. data/lib/slim_lint/configuration.rb +111 -0
  9. data/lib/slim_lint/configuration_loader.rb +86 -0
  10. data/lib/slim_lint/constants.rb +10 -0
  11. data/lib/slim_lint/document.rb +78 -0
  12. data/lib/slim_lint/engine.rb +41 -0
  13. data/lib/slim_lint/exceptions.rb +20 -0
  14. data/lib/slim_lint/file_finder.rb +88 -0
  15. data/lib/slim_lint/filter.rb +126 -0
  16. data/lib/slim_lint/filters/attribute_processor.rb +46 -0
  17. data/lib/slim_lint/filters/auto_indenter.rb +39 -0
  18. data/lib/slim_lint/filters/control_processor.rb +46 -0
  19. data/lib/slim_lint/filters/do_inserter.rb +39 -0
  20. data/lib/slim_lint/filters/end_inserter.rb +74 -0
  21. data/lib/slim_lint/filters/interpolation.rb +73 -0
  22. data/lib/slim_lint/filters/multi_flattener.rb +32 -0
  23. data/lib/slim_lint/filters/splat_processor.rb +20 -0
  24. data/lib/slim_lint/filters/static_merger.rb +47 -0
  25. data/lib/slim_lint/lint.rb +70 -0
  26. data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
  27. data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
  28. data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
  29. data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
  30. data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
  31. data/lib/slim_lint/linter/embedded_engines.rb +18 -0
  32. data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
  33. data/lib/slim_lint/linter/empty_lines.rb +24 -0
  34. data/lib/slim_lint/linter/file_length.rb +18 -0
  35. data/lib/slim_lint/linter/line_length.rb +18 -0
  36. data/lib/slim_lint/linter/redundant_div.rb +21 -0
  37. data/lib/slim_lint/linter/rubocop.rb +131 -0
  38. data/lib/slim_lint/linter/standard.rb +69 -0
  39. data/lib/slim_lint/linter/tab.rb +20 -0
  40. data/lib/slim_lint/linter/tag_case.rb +15 -0
  41. data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
  42. data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
  43. data/lib/slim_lint/linter.rb +93 -0
  44. data/lib/slim_lint/linter_registry.rb +37 -0
  45. data/lib/slim_lint/linter_selector.rb +87 -0
  46. data/lib/slim_lint/logger.rb +103 -0
  47. data/lib/slim_lint/matcher/anything.rb +11 -0
  48. data/lib/slim_lint/matcher/base.rb +21 -0
  49. data/lib/slim_lint/matcher/capture.rb +32 -0
  50. data/lib/slim_lint/matcher/nothing.rb +13 -0
  51. data/lib/slim_lint/options.rb +110 -0
  52. data/lib/slim_lint/parser.rb +584 -0
  53. data/lib/slim_lint/rake_task.rb +125 -0
  54. data/lib/slim_lint/report.rb +25 -0
  55. data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
  56. data/lib/slim_lint/reporter/default_reporter.rb +40 -0
  57. data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
  58. data/lib/slim_lint/reporter/json_reporter.rb +50 -0
  59. data/lib/slim_lint/reporter.rb +44 -0
  60. data/lib/slim_lint/ruby_extract_engine.rb +30 -0
  61. data/lib/slim_lint/ruby_extractor.rb +175 -0
  62. data/lib/slim_lint/ruby_parser.rb +32 -0
  63. data/lib/slim_lint/runner.rb +82 -0
  64. data/lib/slim_lint/sexp.rb +134 -0
  65. data/lib/slim_lint/sexp_visitor.rb +150 -0
  66. data/lib/slim_lint/source_location.rb +45 -0
  67. data/lib/slim_lint/utils.rb +84 -0
  68. data/lib/slim_lint/version.rb +6 -0
  69. data/lib/slim_lint.rb +55 -0
  70. metadata +218 -0
@@ -0,0 +1,584 @@
1
+ module SlimLint
2
+ # This version of the Slim::Parser makes the smallest changes it can to
3
+ # preserve newline informatino through the parse. This helps us keep better
4
+ # track of line numbers.
5
+ class Parser < Slim::Parser
6
+ @options = Slim::Parser.options
7
+
8
+ BLANK_LINE_RE = /\A\s*\Z/
9
+
10
+ def call(str)
11
+ reset(str.split(/\r?\n/))
12
+ push create_container(sexp(:multi, start: [1, 1]))
13
+
14
+ parse_line while next_line
15
+ result = pop until @stacks.empty?
16
+
17
+ reset
18
+ result
19
+ end
20
+
21
+ def append(sexp)
22
+ @stacks.last << sexp
23
+ end
24
+
25
+ def push(sexp)
26
+ @stacks << sexp
27
+ end
28
+
29
+ def pop
30
+ @stacks.last.finish = pos
31
+ @stacks.pop
32
+ end
33
+
34
+ def reset(lines = nil)
35
+ # Since you can indent however you like in Slim, we need to keep a list
36
+ # of how deeply indented you are. For instance, in a template like this:
37
+ #
38
+ # doctype # 0 spaces
39
+ # html # 0 spaces
40
+ # head # 1 space
41
+ # title # 4 spaces
42
+ #
43
+ # indents will then contain [0, 1, 4] (when it's processing the last line.)
44
+ #
45
+ # We uses this information to figure out how many steps we must "jump"
46
+ # out when we see an de-indented line.
47
+ @indents = []
48
+
49
+ # Whenever we want to output something, we'll *always* output it to the
50
+ # last stack in this array. So when there's a line that expects
51
+ # indentation, we simply push a new stack onto this array. When it
52
+ # processes the next line, the content will then be outputted into that
53
+ # stack.
54
+ @stacks = []
55
+
56
+ @lineno = 0
57
+ @lines = lines
58
+ @prev_line = @line = @orig_line = nil
59
+ end
60
+
61
+ def next_line
62
+ @prev_line = @orig_line
63
+ if @lines.empty?
64
+ @orig_line = @line = nil
65
+ else
66
+ @orig_line = @lines.shift
67
+ @lineno += 1
68
+ @line = @orig_line.dup
69
+ end
70
+ end
71
+
72
+ protected
73
+
74
+ def parse_line
75
+ if @line =~ BLANK_LINE_RE
76
+ @line = $'
77
+ return
78
+ end
79
+
80
+ indent = get_indent(@line)
81
+
82
+ # Choose first indentation yourself
83
+ if @indents.empty?
84
+ @indents << indent
85
+ end
86
+
87
+ # Remove the indentation
88
+ @line.lstrip!
89
+
90
+ # If there's more stacks than indents, it means that the previous
91
+ # line is expecting this line to be indented.
92
+ expecting_indentation = @stacks.size > @indents.size
93
+
94
+ if indent > @indents.last
95
+ # This line was actually indented, so we'll have to check if it was
96
+ # supposed to be indented or not.
97
+ syntax_error!("Unexpected indentation") unless expecting_indentation
98
+
99
+ @indents << indent
100
+ else
101
+ # This line was *not* indented more than the line before,
102
+ # so we'll just forget about the stack that the previous line pushed.
103
+ pop if expecting_indentation
104
+
105
+ # This line was deindented.
106
+ # Now we're have to go through the all the indents and figure out
107
+ # how many levels we've deindented.
108
+ while indent < @indents.last && @indents.size > 1
109
+ @indents.pop
110
+ pop
111
+ end
112
+
113
+ # This line's indentation happens to lie "between" two other line's
114
+ # indentation:
115
+ #
116
+ # hello
117
+ # world
118
+ # this # <- This should not be possible!
119
+ syntax_error!("Malformed indentation") if indent != @indents.last
120
+ end
121
+
122
+ case @line
123
+ when /\A\/!( ?)/
124
+ # HTML comment
125
+ comment = sexp(:html, :comment)
126
+
127
+ @line = $'
128
+ text = sexp(:slim, :text, :verbatim)
129
+ capture(text) { parse_text_block([:slim, :interpolate], @line, @indents.last + $1.size + 2) }
130
+ contains(comment, text)
131
+
132
+ append comment
133
+ when /\A\/(\[\s*(.*?)\s*\])\s*\Z/
134
+ # HTML conditional comment
135
+ block = create_container(sexp(:multi))
136
+ comment = create_container(sexp(:html, :condcomment))
137
+ @line.slice!(0, 2)
138
+ comment << atom($2) << block
139
+
140
+ append comment
141
+ push block
142
+ when /\A\//
143
+ # Slim comment
144
+ parse_comment_block
145
+ when /\A([|'])( ?)/
146
+ # Found verbatim text block.
147
+ trailing_ws = ($1 == "'") && sexp(:static, " ", width: 1)
148
+ text = sexp(:slim, :text, :verbatim)
149
+ @line = $'
150
+ capture(text) { parse_text_block([:slim, :interpolate], @line, @indents.last + $2.size + 1) }
151
+
152
+ append text
153
+ append trailing_ws if trailing_ws
154
+ when /\A</
155
+ # Inline html
156
+ block = sexp(:multi)
157
+ html = sexp(:multi)
158
+ interpolation = sexp(:slim, :interpolate)
159
+ capture(interpolation) { @line.tap { @line = "" } }
160
+ contains(html, interpolation)
161
+ contains(html, block)
162
+
163
+ append html
164
+ push block
165
+ when /\A-/
166
+ # Found a code block.
167
+ # We expect the line to be broken or the next line to be indented.
168
+ statement = sexp(:slim, :control)
169
+ @line = $'
170
+ block = sexp(:multi)
171
+ capture(statement) { parse_broken_line }
172
+ statement << block
173
+
174
+ append statement
175
+ push block
176
+ when /\A=(=?)(['<>]*)/
177
+ # Found an output block.
178
+ # We expect the line to be broken or the next line to be indented.
179
+ statement = sexp(:slim, :output, $1.empty?)
180
+ @line = $'
181
+ trailing_ws = $2.include?(">".freeze)
182
+ if $2.include?("'".freeze)
183
+ deprecated_syntax "=' for trailing whitespace is deprecated in favor of =>"
184
+ trailing_ws = true
185
+ end
186
+
187
+ block = sexp(:multi)
188
+ capture(statement) { parse_broken_line }
189
+ statement << block
190
+
191
+ append sexp(:static, " ") if $2.include?("<".freeze)
192
+ append statement
193
+ append sexp(:static, " ") if trailing_ws
194
+ push block
195
+ when @embedded_re
196
+ # Embedded template detected. It is treated as block.
197
+ block = sexp(:slim, :embedded, $1)
198
+ @line = $2
199
+ attrs = parse_attributes
200
+ capture(block) { parse_text_block([:static], $', @orig_line.size - $'.size + $2.size) }
201
+ capture(block) { attrs }
202
+
203
+ append block
204
+ when /\Adoctype\b/
205
+ # Found doctype declaration
206
+ append sexp(:html, :doctype, $'.strip, width: @line.size)
207
+ when @tag_re
208
+ # Found a HTML tag.
209
+ tag_start = pos
210
+ @line = $' if $1
211
+ parse_tag($&, tag_start)
212
+ else
213
+ unknown_line_indicator
214
+ end
215
+ end
216
+
217
+ # Unknown line indicator found. Overwrite this method if
218
+ # you want to add line indicators to the Slim parser.
219
+ # The default implementation throws a syntax error.
220
+ def unknown_line_indicator
221
+ syntax_error! "Unknown line indicator"
222
+ end
223
+
224
+ def parse_comment_block
225
+ while !@lines.empty? && (BLANK_LINE_RE.match?(@lines.first) || get_indent(@lines.first) > @indents.last)
226
+ next_line
227
+ end
228
+ end
229
+
230
+ def parse_text_block(type, first_line = nil, text_indent = nil)
231
+ result = sexp(:multi, start: [@lineno, @indents.last])
232
+ if !first_line || first_line.empty?
233
+ text_indent = nil
234
+ else
235
+ result << sexp(*type, first_line, width: first_line.chomp.size)
236
+ @line = ""
237
+ end
238
+
239
+ until @lines.empty?
240
+ if BLANK_LINE_RE.match?(@lines.first)
241
+ next_line
242
+ result << sexp(*type, "")
243
+ else
244
+ indent = get_indent(@lines.first)
245
+ break if indent <= @indents.last
246
+
247
+ next_line
248
+
249
+ # The text block lines must be at least indented
250
+ # as deep as the first line.
251
+ offset = text_indent ? indent - text_indent : 0
252
+ if offset < 0
253
+ text_indent += offset
254
+ offset = 0
255
+ end
256
+ @line.slice!(0, indent - offset)
257
+
258
+ result << sexp(*type, @line, width: @line.chomp.size)
259
+ @line = ""
260
+
261
+ # The indentation of first line of the text block
262
+ # determines the text base indentation.
263
+ text_indent ||= indent
264
+ end
265
+ end
266
+
267
+ result.finish = pos
268
+ result
269
+ end
270
+
271
+ def parse_broken_line
272
+ result = sexp(:multi)
273
+
274
+ ws = @orig_line[/\A[ \t]*/].size
275
+ @line.lstrip!
276
+
277
+ leader = column - ws - 1
278
+ indent = @indents.last + leader
279
+
280
+ result << sexp(:code, @line, width: @line.chomp.size)
281
+ while @line.strip =~ /[,\\]\Z/
282
+ expect_next_line
283
+ @line.slice!(0, indent)
284
+ result << sexp(:code, @line, width: @line.chomp.size)
285
+ end
286
+
287
+ result
288
+ end
289
+
290
+ def parse_tag(tag_name, tag_start)
291
+ if @tag_shortcut[tag_name]
292
+ @line.slice!(0, tag_name.size) unless @attr_shortcut[tag_name]
293
+ tag_name = @tag_shortcut[tag_name]
294
+ end
295
+
296
+ # Find any shortcut attributes
297
+ attributes = sexp(:html, :attrs)
298
+ while @line =~ @attr_shortcut_re
299
+ # The class/id attribute is :static instead of :slim :interpolate,
300
+ # because we don't want text interpolation in .class or #id shortcut
301
+ syntax_error!("Illegal shortcut") unless (shortcut = @attr_shortcut[$1])
302
+ shortcut.each { |a| attributes << sexp(:html, :attr, a, sexp(:static, $2)) }
303
+ if (additional_attr_pairs = @additional_attrs[$1])
304
+ additional_attr_pairs.each do |k, v|
305
+ attributes << sexp(:html, :attr, k.to_s, sexp(:static, v))
306
+ end
307
+ end
308
+ @line = $'
309
+ end
310
+
311
+ @line =~ /\A[<>']*/
312
+ @line = $'
313
+ trailing_ws = $&.include?(">".freeze)
314
+ if $&.include?("'".freeze)
315
+ deprecated_syntax "tag' for trailing whitespace is deprecated in favor of tag>"
316
+ trailing_ws = true
317
+ end
318
+
319
+ leading_ws = $&.include?("<".freeze)
320
+
321
+ tag = sexp(:html, :tag, tag_name, attributes, start: tag_start, finish: pos)
322
+ parse_attributes(attributes)
323
+
324
+ append sexp(:static, " ") if leading_ws
325
+ append tag
326
+ append sexp(:static, " ") if trailing_ws
327
+
328
+ case @line
329
+ when /\A\s*:\s*/
330
+ # Block expansion
331
+ @line = $'
332
+ if @line =~ @embedded_re
333
+
334
+ # Parse attributes
335
+ @line = $2
336
+ attrs = parse_attributes
337
+ tag << sexp(:slim, :embedded, $1, parse_text_block([:static], $', @orig_line.size - $'.size + $2.size), attrs)
338
+ else
339
+ (@line =~ @tag_re) || syntax_error!("Expected tag")
340
+ tag_start = pos
341
+ @line = $' if $1
342
+ content = sexp(:multi)
343
+ tag << content
344
+ push content
345
+ parse_tag($&, tag_start)
346
+ pop
347
+ end
348
+ when /\A\s*=(=?)(['<>]*)/
349
+ # Handle output code
350
+ statement = sexp(:slim, :output, $1 != "=")
351
+
352
+ @line = $'
353
+ trailing_ws2 = $2.include?(">".freeze)
354
+ if $2.include?("'".freeze)
355
+ deprecated_syntax "=' for trailing whitespace is deprecated in favor of =>"
356
+ trailing_ws2 = true
357
+ end
358
+ block = sexp(:multi)
359
+ capture(statement) { parse_broken_line }
360
+ statement << block
361
+
362
+ @stacks.last.insert(-2, sexp(:static, " ")) if !leading_ws && $2.include?("<".freeze)
363
+ tag << statement
364
+ append sexp(:static, " ") if !trailing_ws && trailing_ws2
365
+ push block
366
+ when /\A\s*\/\s*/
367
+ # Closed tag. Do nothing
368
+ @line = $'
369
+ syntax_error!("Unexpected text after closed tag") unless @line.empty?
370
+ when BLANK_LINE_RE
371
+ # Empty content
372
+ content = sexp(:multi)
373
+ tag << content
374
+ push content
375
+ when /\A ?/
376
+ # Text content
377
+ @line = $'
378
+ tag << sexp(:slim, :text, :inline)
379
+ tag.last << parse_text_block([:slim, :interpolate], $', @orig_line.size - $'.size)
380
+ tag.last.finish = pos
381
+ end
382
+ end
383
+
384
+ def parse_attributes(attributes = sexp(:html, :attrs))
385
+ # Check to see if there is a delimiter right after the tag name
386
+ delimiter = nil
387
+ if @line =~ @attr_list_delims_re
388
+ delimiter = @attr_list_delims[$1]
389
+ @line = $'
390
+ end
391
+
392
+ if delimiter
393
+ boolean_attr_re = /#{@attr_name}(?=(\s|#{Regexp.escape delimiter}|\Z))/
394
+ end_re = /\A\s*#{Regexp.escape delimiter}/
395
+ end
396
+
397
+ loop do
398
+ case @line.strip
399
+ when @splat_attrs_regexp
400
+ # Splat attribute
401
+ @line.lstrip!
402
+ splat = sexp(:slim, :splat)
403
+ @line = $'
404
+ capture(splat) { parse_ruby_code(delimiter) }
405
+ attributes << splat
406
+ when @quoted_attr_re
407
+ # Value is quoted (static)
408
+ @line.lstrip!
409
+ attr = sexp(:html, :attr, $1)
410
+ @line = $3 + $'
411
+
412
+ escape = sexp(:escape, $2.empty?)
413
+ interpolate = sexp(:slim, :interpolate)
414
+ value = parse_quoted_attribute($3)
415
+ attributes.finish = attr.finish = escape.finish = interpolate.finish = pos
416
+
417
+ attributes << attr
418
+ attr << escape
419
+ escape << interpolate
420
+ interpolate << value
421
+ when @code_attr_re
422
+ # Value is ruby code
423
+ @line.lstrip!
424
+ attr = sexp(:html, :attr, $1)
425
+ @line = $'
426
+
427
+ value = ""
428
+ attr_value = sexp(:slim, :attrvalue, $2.empty?)
429
+ capture(attr_value) { value = parse_ruby_code(delimiter) }
430
+ attr << attr_value
431
+ syntax_error!("Invalid empty attribute") if value.empty?
432
+ attributes << attr
433
+ else
434
+ break unless delimiter
435
+
436
+ case @line
437
+ when boolean_attr_re
438
+ # Boolean attribute
439
+ @line = $'
440
+ attributes << sexp(:html, :attr, $1, sexp(:multi))
441
+ when end_re
442
+ # Find ending delimiter
443
+ @line = $'
444
+ break
445
+ else
446
+ # Found something where an attribute should be
447
+ @line.lstrip!
448
+ syntax_error!("Expected attribute") unless @line.empty?
449
+
450
+ # Attributes span multiple lines
451
+ syntax_error!("Expected closing delimiter #{delimiter}") if @lines.empty?
452
+ next_line
453
+ end
454
+ end
455
+ end
456
+
457
+ attributes
458
+ end
459
+
460
+ def parse_ruby_code(outer_delimiter)
461
+ result = sexp(:multi)
462
+ count, delimiter, close_delimiter = 0, nil, nil
463
+
464
+ # Attribute ends with space or attribute delimiter
465
+ end_re = /\A[\s#{Regexp.escape outer_delimiter.to_s}]/
466
+
467
+ indent = column
468
+ code = ""
469
+ until @line.empty? || (count == 0 && @line =~ end_re)
470
+ if @line == "," || @line == "\\"
471
+ code << @line
472
+ result << sexp(:code, code, start: [@lineno, indent], width: code.size)
473
+ expect_next_line
474
+ code = ""
475
+ @line.sub!(/\A {,#{indent - 1}}/, "")
476
+ else
477
+ if count > 0
478
+ if @line[0] == delimiter[0]
479
+ count += 1
480
+ elsif @line[0] == close_delimiter[0]
481
+ count -= 1
482
+ end
483
+ elsif @line =~ @code_attr_delims_re
484
+ count = 1
485
+ delimiter, close_delimiter = $&, @code_attr_delims[$&]
486
+ end
487
+ code << @line.slice!(0)
488
+ end
489
+ end
490
+ syntax_error!("Expected closing delimiter #{close_delimiter}") if count != 0
491
+
492
+ result << sexp(:code, code, start: [@lineno, indent], width: code.size)
493
+ result.finish = result.last.finish
494
+ result
495
+ end
496
+
497
+ def parse_quoted_attribute(quote)
498
+ @line.slice!(0)
499
+ start_pos = pos
500
+ value, count = "", 0
501
+
502
+ until count == 0 && @line[0] == quote[0]
503
+ if @line =~ /\A(\\)?\Z/
504
+ value << ($1 ? " " : "\n")
505
+ expect_next_line
506
+ @line.strip!
507
+ else
508
+ if @line[0] == "{"
509
+ count += 1
510
+ elsif @line[0] == "}"
511
+ count -= 1
512
+ end
513
+ value << @line.slice!(0)
514
+ end
515
+ end
516
+
517
+ atom(value, pos: start_pos)
518
+ ensure
519
+ @line.slice!(0)
520
+ end
521
+
522
+ # Helper for raising exceptions
523
+ def syntax_error!(message)
524
+ raise SyntaxError.new(message, options[:file], @orig_line, @lineno, column)
525
+ rescue SyntaxError => ex
526
+ # HACK: Manipulate stacktrace for Rails and other frameworks
527
+ # to find the right file.
528
+ ex.backtrace.unshift "#{options[:file]}:#{@lineno}"
529
+ raise
530
+ end
531
+
532
+ def deprecated_syntax(message)
533
+ line = @orig_line.lstrip
534
+ warn %(Deprecated syntax: #{message}
535
+ #{options[:file]}, Line #{@lineno}, Column #{column}
536
+ #{line}
537
+ #{" " * column}^
538
+ )
539
+ end
540
+
541
+ def expect_next_line
542
+ next_line || syntax_error!("Unexpected end of file")
543
+ @line
544
+ end
545
+
546
+ def pos
547
+ [@lineno, column]
548
+ end
549
+
550
+ def column
551
+ 1 + (@orig_line&.size || 0) - (@line&.size || 0)
552
+ end
553
+
554
+ def sexp(*args, start: pos, finish: start, width: nil, lines: 0)
555
+ finish = [start[0] + lines, start[1] + width] if width
556
+ Sexp.new(*args, start: start, finish: finish)
557
+ end
558
+
559
+ def atom(value, pos: nil)
560
+ Atom.new(value, pos: pos || self.pos)
561
+ end
562
+
563
+ def capture(sexp)
564
+ start = pos
565
+ yielded = yield
566
+ yielded = Atom.new(yielded, pos: start) unless yielded.is_a?(Sexp)
567
+
568
+ sexp << yielded
569
+ sexp.finish = pos
570
+ sexp
571
+ end
572
+
573
+ def create_container(sexp)
574
+ sexp.tap do |container|
575
+ container.define_singleton_method(:finish) { last.finish }
576
+ end
577
+ end
578
+
579
+ def contains(container, content)
580
+ create_container(container)
581
+ container << content
582
+ end
583
+ end
584
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/tasklib"
5
+ require "slim_lint/constants"
6
+
7
+ module SlimLint
8
+ # Rake task interface for slim-lint-standard command line interface.
9
+ #
10
+ # @example
11
+ # # Add the following to your Rakefile...
12
+ # require 'slim_lint/rake_task'
13
+ #
14
+ # SlimLint::RakeTask.new do |t|
15
+ # t.config = 'path/to/custom/slim-lint.yml'
16
+ # t.files = %w[app/views/**/*.slim custom/*.slim]
17
+ # t.quiet = true # Don't display output from slim-lint-standard
18
+ # end
19
+ #
20
+ # # ...and then execute from the command line:
21
+ # rake slim_lint
22
+ #
23
+ # You can also specify the list of files as explicit task arguments:
24
+ #
25
+ # @example
26
+ # # Add the following to your Rakefile...
27
+ # require 'slim_lint/rake_task'
28
+ #
29
+ # SlimLint::RakeTask.new
30
+ #
31
+ # # ...and then execute from the command line (single quotes prevent shell
32
+ # # glob expansion and allow us to have a space after commas):
33
+ # rake 'slim_lint[app/views/**/*.slim, other_files/**/*.slim]'
34
+ #
35
+ class RakeTask < Rake::TaskLib
36
+ # Name of the task.
37
+ # @return [String]
38
+ attr_accessor :name
39
+
40
+ # Configuration file to use.
41
+ # @return [String]
42
+ attr_accessor :config
43
+
44
+ # List of files to lint (can contain shell globs).
45
+ #
46
+ # Note that this will be ignored if you explicitly pass a list of files as
47
+ # task arguments via the command line or a task definition.
48
+ # @return [Array<String>]
49
+ attr_accessor :files
50
+
51
+ # Whether output from slim-lint-standard should not be displayed to the
52
+ # standard out stream.
53
+ # @return [true,false]
54
+ attr_accessor :quiet
55
+
56
+ # Create the task so it exists in the current namespace.
57
+ #
58
+ # @param name [Symbol] task name
59
+ def initialize(name = :slim_lint)
60
+ @name = name
61
+ @files = ["."] # Search for everything under current directory by default
62
+ @quiet = false
63
+
64
+ yield self if block_given?
65
+
66
+ define
67
+ end
68
+
69
+ private
70
+
71
+ # Defines the Rake task.
72
+ def define
73
+ desc default_description unless ::Rake.application.last_description
74
+
75
+ task(name, [:files]) do |_task, task_args|
76
+ # Lazy-load so task doesn't affect Rakefile load time
77
+ require "slim_lint"
78
+ require "slim_lint/cli"
79
+
80
+ run_cli(task_args)
81
+ end
82
+ end
83
+
84
+ # Executes the CLI given the specified task arguments.
85
+ #
86
+ # @param task_args [Rake::TaskArguments]
87
+ def run_cli(task_args)
88
+ cli_args = ["--config", config] if config
89
+
90
+ logger = quiet ? SlimLint::Logger.silent : SlimLint::Logger.new($stdout)
91
+ result = SlimLint::CLI.new(logger).run(Array(cli_args) + files_to_lint(task_args))
92
+
93
+ fail "#{SlimLint::APP_NAME} failed with exit code #{result}" unless result == 0
94
+ end
95
+
96
+ # Returns the list of files that should be linted given the specified task
97
+ # arguments.
98
+ #
99
+ # @param task_args [Rake::TaskArguments]
100
+ def files_to_lint(task_args)
101
+ # Note: we're abusing Rake's argument handling a bit here. We call the
102
+ # first argument `files` but it's actually only the first file--we pull
103
+ # the rest out of the `extras` from the task arguments. This is so we
104
+ # can specify an arbitrary list of files separated by commas on the
105
+ # command line or in a custom task definition.
106
+ explicit_files = Array(task_args[:files]) + Array(task_args.extras)
107
+
108
+ explicit_files.any? ? explicit_files : files
109
+ end
110
+
111
+ # Friendly description that shows the full command that will be executed.
112
+ #
113
+ # This allows us to change the information displayed by `rake --tasks` based
114
+ # on the options passed to the constructor which defined the task.
115
+ #
116
+ # @return [String]
117
+ def default_description
118
+ description = "Run `#{SlimLint::APP_NAME}"
119
+ description += " --config #{config}" if config
120
+ description += " #{files.join(" ")}" if files.any?
121
+ description += " [files...]`"
122
+ description
123
+ end
124
+ end
125
+ end