slim_lint_standard 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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