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.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/bin/slim-lint-standard +7 -0
- data/config/default.yml +109 -0
- data/lib/slim_lint/atom.rb +129 -0
- data/lib/slim_lint/capture_map.rb +19 -0
- data/lib/slim_lint/cli.rb +167 -0
- data/lib/slim_lint/configuration.rb +111 -0
- data/lib/slim_lint/configuration_loader.rb +86 -0
- data/lib/slim_lint/constants.rb +10 -0
- data/lib/slim_lint/document.rb +78 -0
- data/lib/slim_lint/engine.rb +41 -0
- data/lib/slim_lint/exceptions.rb +20 -0
- data/lib/slim_lint/file_finder.rb +88 -0
- data/lib/slim_lint/filter.rb +126 -0
- data/lib/slim_lint/filters/attribute_processor.rb +46 -0
- data/lib/slim_lint/filters/auto_indenter.rb +39 -0
- data/lib/slim_lint/filters/control_processor.rb +46 -0
- data/lib/slim_lint/filters/do_inserter.rb +39 -0
- data/lib/slim_lint/filters/end_inserter.rb +74 -0
- data/lib/slim_lint/filters/interpolation.rb +73 -0
- data/lib/slim_lint/filters/multi_flattener.rb +32 -0
- data/lib/slim_lint/filters/splat_processor.rb +20 -0
- data/lib/slim_lint/filters/static_merger.rb +47 -0
- data/lib/slim_lint/lint.rb +70 -0
- data/lib/slim_lint/linter/avoid_multiline_expressions.rb +41 -0
- data/lib/slim_lint/linter/comment_control_statement.rb +26 -0
- data/lib/slim_lint/linter/consecutive_control_statements.rb +26 -0
- data/lib/slim_lint/linter/control_statement_spacing.rb +32 -0
- data/lib/slim_lint/linter/dynamic_output_spacing.rb +77 -0
- data/lib/slim_lint/linter/embedded_engines.rb +18 -0
- data/lib/slim_lint/linter/empty_control_statement.rb +15 -0
- data/lib/slim_lint/linter/empty_lines.rb +24 -0
- data/lib/slim_lint/linter/file_length.rb +18 -0
- data/lib/slim_lint/linter/line_length.rb +18 -0
- data/lib/slim_lint/linter/redundant_div.rb +21 -0
- data/lib/slim_lint/linter/rubocop.rb +131 -0
- data/lib/slim_lint/linter/standard.rb +69 -0
- data/lib/slim_lint/linter/tab.rb +20 -0
- data/lib/slim_lint/linter/tag_case.rb +15 -0
- data/lib/slim_lint/linter/trailing_blank_lines.rb +19 -0
- data/lib/slim_lint/linter/trailing_whitespace.rb +17 -0
- data/lib/slim_lint/linter.rb +93 -0
- data/lib/slim_lint/linter_registry.rb +37 -0
- data/lib/slim_lint/linter_selector.rb +87 -0
- data/lib/slim_lint/logger.rb +103 -0
- data/lib/slim_lint/matcher/anything.rb +11 -0
- data/lib/slim_lint/matcher/base.rb +21 -0
- data/lib/slim_lint/matcher/capture.rb +32 -0
- data/lib/slim_lint/matcher/nothing.rb +13 -0
- data/lib/slim_lint/options.rb +110 -0
- data/lib/slim_lint/parser.rb +584 -0
- data/lib/slim_lint/rake_task.rb +125 -0
- data/lib/slim_lint/report.rb +25 -0
- data/lib/slim_lint/reporter/checkstyle_reporter.rb +42 -0
- data/lib/slim_lint/reporter/default_reporter.rb +40 -0
- data/lib/slim_lint/reporter/emacs_reporter.rb +40 -0
- data/lib/slim_lint/reporter/json_reporter.rb +50 -0
- data/lib/slim_lint/reporter.rb +44 -0
- data/lib/slim_lint/ruby_extract_engine.rb +30 -0
- data/lib/slim_lint/ruby_extractor.rb +175 -0
- data/lib/slim_lint/ruby_parser.rb +32 -0
- data/lib/slim_lint/runner.rb +82 -0
- data/lib/slim_lint/sexp.rb +134 -0
- data/lib/slim_lint/sexp_visitor.rb +150 -0
- data/lib/slim_lint/source_location.rb +45 -0
- data/lib/slim_lint/utils.rb +84 -0
- data/lib/slim_lint/version.rb +6 -0
- data/lib/slim_lint.rb +55 -0
- 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
|