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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Represents a parsed Slim document and its associated metadata.
5
+ class Document
6
+ FRONTMATTER_RE = /
7
+ # From the start of the string
8
+ \A
9
+ # First-capture match --- followed by optional whitespace up
10
+ # to a newline then 0 or more chars followed by an optional newline.
11
+ # This matches the --- and the contents of the frontmatter
12
+ (---\s*\n.*?\n?)
13
+ # From the start of the line
14
+ ^
15
+ # Second capture match --- or ... followed by optional whitespace
16
+ # and newline. This matches the closing --- for the frontmatter.
17
+ (---|\.\.\.)\s*$\n?
18
+ /mx
19
+
20
+ # @return [SlimLint::Configuration] Configuration used to parse template
21
+ attr_reader :config
22
+
23
+ # @return [String] Slim template file path
24
+ attr_reader :file
25
+
26
+ # @return [SlimLint::Sexp] Sexpression representing the parsed document
27
+ attr_reader :sexp
28
+
29
+ # @return [String] original source code
30
+ attr_reader :source
31
+
32
+ # @return [Array<String>] original source code as an array of lines
33
+ attr_reader :source_lines
34
+
35
+ # Parses the specified Slim code into a {Document}.
36
+ #
37
+ # @param source [String] Slim code to parse
38
+ # @param options [Hash]
39
+ # @option options :file [String] file name of document that was parsed
40
+ # @raise [Slim::Parser::Error] if there was a problem parsing the document
41
+ def initialize(source, options)
42
+ @config = options[:config]
43
+ @file = options.fetch(:file, nil)
44
+
45
+ process_source(source)
46
+ end
47
+
48
+ private
49
+
50
+ # @param source [String] Slim code to parse
51
+ # @raise [SlimLint::Exceptions::ParseError] if there was a problem parsing the document
52
+ def process_source(source)
53
+ @source = process_encoding(source)
54
+ @source = strip_frontmatter(source)
55
+ @source_lines = @source.split("\n")
56
+
57
+ engine = SlimLint::Engine.new(file: @file)
58
+ @sexp = engine.parse(@source)
59
+ end
60
+
61
+ # Ensure the string's encoding is valid.
62
+ #
63
+ # @param source [String]
64
+ # @return [String] source encoded in a valid encoding
65
+ def process_encoding(source)
66
+ ::Temple::Filters::Encoding.new.call(source)
67
+ end
68
+
69
+ # Removes YAML frontmatter
70
+ def strip_frontmatter(source)
71
+ if config["skip_frontmatter"] && source =~ FRONTMATTER_RE
72
+ source = $POSTMATCH
73
+ end
74
+
75
+ source
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ # Temple engine used to generate a {Sexp} parse tree for use by linters.
5
+ #
6
+ # We omit a lot of the filters that are in {Slim::Engine} because they result
7
+ # in information potentially being removed from the parse tree (since some
8
+ # Sexp abstractions are optimized/removed or otherwise transformed). In order
9
+ # for linters to be useful, they need to operate on the original parse tree.
10
+ #
11
+ # The other key task this engine accomplishes is converting the Array-based
12
+ # S-expressions into {SlimLint::Sexp} objects, which have a number of helper
13
+ # methods that makes working with them easier. It also annotates these
14
+ # {SlimLint::Sexp} objects with line numbers so it's easy to cross reference
15
+ # with the original source code.
16
+ class Engine < Temple::Engine
17
+ filter :Encoding
18
+ filter :RemoveBOM
19
+
20
+ # Parse into S-expression using Slim parser
21
+ use SlimLint::Parser
22
+
23
+ # Parses the given source code into a Sexp.
24
+ #
25
+ # @param source [String] source code to parse
26
+ # @return [SlimLint::Sexp] parsed Sexp
27
+ def parse(source)
28
+ call(source)
29
+ rescue ::Slim::Parser::SyntaxError => e
30
+ # Convert to our own exception type to isolate from upstream changes
31
+ error = SlimLint::Exceptions::ParseError.new(
32
+ e.error,
33
+ e.file,
34
+ e.line,
35
+ e.lineno,
36
+ e.column
37
+ )
38
+ raise error
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection of exceptions that can be raised by the application.
4
+ module SlimLint::Exceptions
5
+ # Raised when a {Configuration} could not be loaded from a file.
6
+ class ConfigurationError < StandardError; end
7
+
8
+ # Raised when invalid/incompatible command line options are provided.
9
+ class InvalidCLIOption < StandardError; end
10
+
11
+ # Raised when an invalid file path is specified
12
+ class InvalidFilePath < StandardError; end
13
+
14
+ # Raised when the Slim parser is unable to parse a template.
15
+ class ParseError < ::Slim::Parser::SyntaxError; end
16
+
17
+ # Raised when attempting to execute `Runner` with options that would result in
18
+ # no linters being enabled.
19
+ class NoLintersError < StandardError; end
20
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "find"
4
+
5
+ module SlimLint
6
+ # Finds Slim files that should be linted given a specified list of paths, glob
7
+ # patterns, and configuration.
8
+ class FileFinder
9
+ # List of extensions of files to include under a directory when a directory
10
+ # is specified instead of a file.
11
+ VALID_EXTENSIONS = %w[.slim].freeze
12
+
13
+ # Create a file finder using the specified configuration.
14
+ #
15
+ # @param config [SlimLint::Configuration]
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ # Return list of files to lint given the specified set of paths and glob
21
+ # patterns.
22
+ # @param patterns [Array<String>]
23
+ # @param excluded_patterns [Array<String>]
24
+ # @raise [SlimLint::Exceptions::InvalidFilePath]
25
+ # @return [Array<String>] list of actual files
26
+ def find(patterns, excluded_patterns)
27
+ excluded_patterns = excluded_patterns.map { |pattern| normalize_path(pattern) }
28
+
29
+ extract_files_from(patterns).reject do |file|
30
+ SlimLint::Utils.any_glob_matches?(excluded_patterns, file)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Extract the list of matching files given the list of glob patterns, file
37
+ # paths, and directories.
38
+ #
39
+ # @param patterns [Array<String>]
40
+ # @return [Array<String>]
41
+ def extract_files_from(patterns)
42
+ files = []
43
+
44
+ patterns.each do |pattern|
45
+ if File.file?(pattern)
46
+ files << pattern
47
+ else
48
+ begin
49
+ ::Find.find(pattern) do |file|
50
+ files << file if slim_file?(file)
51
+ end
52
+ rescue ::Errno::ENOENT
53
+ # File didn't exist; it might be a file glob pattern
54
+ matches = ::Dir.glob(pattern)
55
+ if matches.any?
56
+ files += matches
57
+ else
58
+ # One of the paths specified does not exist; raise a more
59
+ # descriptive exception so we know which one
60
+ raise SlimLint::Exceptions::InvalidFilePath,
61
+ "File path '#{pattern}' does not exist"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ files.uniq.sort.map { |file| normalize_path(file) }
68
+ end
69
+
70
+ # Trim "./" from the front of relative paths.
71
+ #
72
+ # @param path [String]
73
+ # @return [String]
74
+ def normalize_path(path)
75
+ path.start_with?(".#{File::SEPARATOR}") ? path[2..] : path
76
+ end
77
+
78
+ # Whether the given file should be treated as a Slim file.
79
+ #
80
+ # @param file [String]
81
+ # @return [Boolean]
82
+ def slim_file?(file)
83
+ return false unless ::FileTest.file?(file)
84
+
85
+ VALID_EXTENSIONS.include?(::File.extname(file))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,126 @@
1
+ module SlimLint
2
+ # Alternative implementation of Slim::Filter that operates without
3
+ # destroying the Sexp position data.
4
+ class Filter < Temple::HTML::Filter
5
+ module Overrides
6
+ def on_multi(*exps)
7
+ exps.each.with_index(1) { |exp, i| @self[i] = compile(exp) }
8
+ @self
9
+ end
10
+
11
+ def on_escape(flag, content)
12
+ @self[2] = compile(content)
13
+ @self
14
+ end
15
+
16
+ def on_html_attrs(*attrs)
17
+ attrs.each.with_index(2) { |attr, i| @self[i] = compile(attr) }
18
+ @self
19
+ end
20
+
21
+ def on_html_attr(name, content)
22
+ @self[3] = compile(content)
23
+ @self
24
+ end
25
+
26
+ def on_html_comment(content)
27
+ @self[2] = compile(content)
28
+ @self
29
+ end
30
+
31
+ def on_html_condcomment(condition, content)
32
+ @self[3] = compile(content)
33
+ @self
34
+ end
35
+
36
+ def on_html_js(content)
37
+ @self[2] = compile(content)
38
+ @self
39
+ end
40
+
41
+ def on_html_tag(name, attrs, content = nil)
42
+ @self[3] = compile(attrs)
43
+ @self[4] = compile(content) if content
44
+ @self
45
+ end
46
+
47
+ # Pass-through handler
48
+ def on_slim_text(type, content)
49
+ @self[3] = compile(content)
50
+ @self
51
+ end
52
+
53
+ # Pass-through handler
54
+ def on_slim_embedded(type, content, attrs)
55
+ @self[3] = compile(content)
56
+ @self
57
+ end
58
+
59
+ # Pass-through handler
60
+ def on_slim_control(code, content)
61
+ @self[3] = compile(content)
62
+ @self
63
+ end
64
+
65
+ # Pass-through handler
66
+ def on_slim_output(escape, code, content)
67
+ @self[4] = compile(content)
68
+ @self
69
+ end
70
+
71
+ private
72
+
73
+ def dispatcher(exp)
74
+ @self_stack ||= []
75
+ @key_stack ||= []
76
+ @self_stack << @self
77
+ @self = exp
78
+
79
+ exp.size.downto(1) do |depth|
80
+ available_methods = dispatched_methods_by_depth[depth]
81
+ next unless available_methods
82
+
83
+ slice = exp.take(depth)
84
+ next unless slice.all? { |x| x.is_a?(Atom) && x.value.is_a?(Symbol) }
85
+
86
+ name = "on_#{slice.join("_")}"
87
+ if available_methods.include?(name)
88
+ @key_stack << @key
89
+ @key = slice
90
+ return send(name, *exp.drop(depth))
91
+ end
92
+ end
93
+
94
+ exp
95
+ ensure
96
+ @self = @self_stack.pop
97
+ @key = @key_stack.pop
98
+ end
99
+
100
+ def dispatched_methods_by_depth
101
+ @dispatched_methods_by_depth ||= dispatched_methods.group_by { |x| x.count("_") }
102
+ end
103
+
104
+ def empty_exp?(exp)
105
+ case exp[0].value
106
+ when :multi
107
+ exp[1..].all? { |e| empty_exp?(e) }
108
+ else
109
+ false
110
+ end
111
+ end
112
+
113
+ # Compares two [line, column] position pairs, and returns true if position
114
+ # `a` comes before position `b`.
115
+ #
116
+ # @param a [Array(Int, Int)] Position `a`
117
+ # @param b [Array(Int, Int)] Position `b`
118
+ # @return Does position `a` occur before position `b`?
119
+ def later_pos?(a, b)
120
+ a[0] < b[0] || (a[0] == b[0] && a[1] < b[1])
121
+ end
122
+ end
123
+
124
+ include Overrides
125
+ end
126
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ module Filters
5
+ # A dumbed-down version of {Slim::CodeAttributes} which doesn't introduce any
6
+ # temporary variables or other cruft.
7
+ class AttributeProcessor < Filter
8
+ define_options :merge_attrs
9
+
10
+ # Handle attributes expression `[:html, :attrs, *attrs]`
11
+ #
12
+ # @param attrs [Array]
13
+ # @return [Array]
14
+ def on_html_attrs(*attrs)
15
+ @self.delete_at(1)
16
+ expr = on_multi(*attrs)
17
+ expr[0].value = :multi
18
+ expr
19
+ end
20
+
21
+ # # Handle attribute expression `[:html, :attr, name, value]`
22
+ # #
23
+ # # @param name [String] name of the attribute
24
+ # # @param value [Array] Sexp representing the value
25
+ # def on_html_attr(name, value)
26
+ # if value[0] == :slim && value[1] == :attrvalue
27
+ # code = value[3]
28
+ # [:code, code]
29
+ # else
30
+ # @attr = name
31
+ # super
32
+ # end
33
+ # end
34
+
35
+ def on_slim_attrvalue(_escape, code)
36
+ return code if code[0] == :multi
37
+ @self.start = code.start
38
+ @self.finish = code.finish
39
+ @self[0].value = :code
40
+ @self.delete_at(2)
41
+ @self.delete_at(1)
42
+ @self
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ module SlimLint
2
+ module Filters
3
+ # This filter annotates the sexp with indentation guidance, so that we can
4
+ # generate Ruby code with reasonable indentation semantics.
5
+ class AutoIndenter < Filter
6
+ BLOCK_REGEX = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\Z/
7
+
8
+ # Handle control expression `[:slim, :control, code, content]`
9
+ #
10
+ # @param [String] code Ruby code
11
+ # @param [Array] content Temple expression
12
+ # @return [Array] Compiled temple expression
13
+ def on_slim_control(code, content)
14
+ @self[3] = compile(content)
15
+ if code.last.last.value =~ BLOCK_REGEX && content[0].value == :multi
16
+ @self[3].insert(1, Sexp.new(:slim_lint, :indent, start: content.start, finish: content.start))
17
+ @self[3].insert(-1, Sexp.new(:slim_lint, :outdent, start: content.finish, finish: content.finish))
18
+ end
19
+
20
+ @self
21
+ end
22
+
23
+ # Handle output expression `[:slim, :control, escape, code, content]`
24
+ #
25
+ # @param [String] code Ruby code
26
+ # @param [Array] content Temple expression
27
+ # @return [Array] Compiled temple expression
28
+ def on_slim_output(escape, code, content)
29
+ @self[4] = compile(content)
30
+ if code.last.last.value =~ BLOCK_REGEX && content[0].value == :multi
31
+ @self[4].insert(1, Sexp.new(:slim_lint, :indent, start: content.start, finish: content.start))
32
+ @self[4].insert(-1, Sexp.new(:slim_lint, :outdent, start: content.finish, finish: content.finish))
33
+ end
34
+
35
+ @self
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlimLint
4
+ module Filters
5
+ # A dumbed-down version of {Slim::Controls} which doesn't introduce temporary
6
+ # variables and other cruft (which in the context of extracting Ruby code,
7
+ # results in a lot of weird cops reported by RuboCop).
8
+ class ControlProcessor < Filter
9
+ BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
10
+
11
+ # Handle output expression `[:slim, :output, escape, code, content]`
12
+ #
13
+ # @param _escape [Boolean]
14
+ # @param code [Sexp]
15
+ # @param content [Sexp]
16
+ # @return [Sexp]
17
+ def on_slim_output(_escape, code, content)
18
+ _, lines = code
19
+
20
+ code.start = @self.start
21
+ code.finish = @self.finish
22
+ code << compile(content)
23
+
24
+ if lines.last[BLOCK_RE]
25
+ code << Sexp.new(Atom.new(:code, pos: code.finish), "end", start: code.finish, finish: code.finish)
26
+ end
27
+
28
+ Sexp.new(
29
+ Atom.new(:dynamic, pos: code.start),
30
+ code,
31
+ start: code.start,
32
+ finish: code.finish
33
+ )
34
+ end
35
+
36
+ # Handle text expression `[:slim, :text, type, content]`
37
+ #
38
+ # @param _type [Symbol]
39
+ # @param content [Sexp]
40
+ # @return [Sexp]
41
+ def on_slim_text(_type, content)
42
+ compile(content)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ module SlimLint
2
+ module Filters
3
+ # In Slim you don't need the do keyword sometimes. This
4
+ # filter adds the missing keyword.
5
+ #
6
+ # - 10.times
7
+ # | Hello
8
+ #
9
+ # @api private
10
+ class DoInserter < Filter
11
+ BLOCK_REGEX = /(\A(if|unless|else|elsif|when|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\Z/
12
+
13
+ # Handle control expression `[:slim, :control, code, content]`
14
+ #
15
+ # @param [Sexp] code Ruby code
16
+ # @param [Sexp] content Temple expression
17
+ # @return [Sexp] Compiled temple expression
18
+ def on_slim_control(code, content)
19
+ _, lines = code
20
+ lines.last.value.concat(" do") unless lines.last.value =~ BLOCK_REGEX || empty_exp?(content)
21
+ @self[3] = compile(content)
22
+ @self
23
+ end
24
+
25
+ # Handle output expression `[:slim, :output, escape, code, content]`
26
+ #
27
+ # @param [Boolean] escape Escape html
28
+ # @param [Sexp] code Ruby code
29
+ # @param [Sexp] content Temple expression
30
+ # @return [Sexp] Compiled temple expression
31
+ def on_slim_output(escape, code, content)
32
+ _, lines = code
33
+ lines.last.value.concat(" do") unless lines.last.value =~ BLOCK_REGEX || empty_exp?(content)
34
+ @self[4] = compile(content)
35
+ @self
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,74 @@
1
+ module SlimLint
2
+ module Filters
3
+ # In Slim you don't need to close any blocks:
4
+ #
5
+ # - if Slim.awesome?
6
+ # | But of course it is!
7
+ #
8
+ # However, the parser is not smart enough (and that's a good thing) to
9
+ # automatically insert end's where they are needed. Luckily, this filter
10
+ # does *exactly* that (and it does it well!)
11
+ #
12
+ # @api private
13
+ class EndInserter < Filter
14
+ IF_RE = /\A(if|begin|unless|else|elsif|when|rescue|ensure)\b|\bdo\s*(\|[^|]*\|)?\s*$/
15
+ ELSE_RE = /\A(else|elsif|when|rescue|ensure)\b/
16
+ END_RE = /\Aend\b/
17
+
18
+ # Handle multi expression `[:multi, *exps]`
19
+ #
20
+ # @return [Sexp] Corrected Temple expression with ends inserted
21
+ def on_multi(*exps)
22
+ @self.clear
23
+ @self.concat(@key)
24
+
25
+ # This variable is true if the previous line was
26
+ # (1) a control code and (2) contained indented content.
27
+ prev_indent = false
28
+
29
+ exps.each do |exp|
30
+ if control?(exp)
31
+ code_frags = exp[2].last
32
+ statement = code_frags.last.value
33
+ raise(Temple::FilterError, "Explicit end statements are forbidden") if END_RE.match?(statement)
34
+
35
+ # Two control code in a row. If this one is *not*
36
+ # an else block, we should close the previous one.
37
+ if prev_indent && statement !~ ELSE_RE
38
+ @self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
39
+ end
40
+
41
+ # Indent if the control code starts a block.
42
+ prev_indent = (statement =~ IF_RE) && exp
43
+ elsif prev_indent
44
+ # This is *not* a control code, so we should close the previous one.
45
+ # Ignores newlines because they will be inserted after each line.
46
+ @self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
47
+ prev_indent = false
48
+ end
49
+
50
+ @self << compile(exp)
51
+ end
52
+
53
+ # The last line can be a control code too.
54
+ if prev_indent
55
+ @self << Sexp.new(:code, "end", start: prev_indent.start, finish: prev_indent.start)
56
+ end
57
+
58
+ @self
59
+ end
60
+
61
+ private
62
+
63
+ # Checks if an expression is a Slim control code
64
+ def control?(exp)
65
+ exp[0].value == :slim && exp[1].value == :control
66
+ end
67
+
68
+ # Checks if an expression is Slim embedded code
69
+ def embedded?(exp)
70
+ exp[0].value == :slim && exp[1].value == :embedded
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,73 @@
1
+ module SlimLint
2
+ module Filters
3
+ # Alternative implementation of Slim::Interpolation that operates without
4
+ # destroying the Sexp position data.
5
+ #
6
+ # @api private
7
+ class Interpolation < Filter
8
+ # Handle interpolate expression `[:slim, :interpolate, string]`
9
+ #
10
+ # @param [String] string Static interpolate
11
+ # @return [Array] Compiled temple expression
12
+ def on_slim_interpolate(string)
13
+ # Interpolate variables in text (#{variable}).
14
+ # Split the text into multiple dynamic and static parts.
15
+ block = Sexp.new(:multi, start: @self.start, finish: @self.finish)
16
+ line, column = string.start
17
+ string = string.to_s
18
+ loop do
19
+ case string
20
+ when /\A\\#\{/
21
+ # Escaped interpolation
22
+ block << Sexp.new(:static, '#{', start: [line, column], finish: [line, (column += 2)])
23
+ string = $'
24
+ when /\A#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/
25
+ # Interpolation
26
+ _, string, code = $&, $', $1
27
+ escape = code !~ /\A\{.*\}\Z/
28
+
29
+ column += 2
30
+ unless escape
31
+ code = code[1..-2]
32
+ column += 1
33
+ end
34
+
35
+ start = [line, column]
36
+ finish = [line, column + code.size]
37
+
38
+ block << Sexp.new(
39
+ :slim,
40
+ :output,
41
+ escape,
42
+ Sexp.new(
43
+ :multi,
44
+ Sexp.new(:interpolated, code, start: start, finish: finish),
45
+ start: start,
46
+ finish: finish
47
+ ),
48
+ Sexp.new(:multi, start: start, finish: finish),
49
+ start: start,
50
+ finish: finish
51
+ )
52
+
53
+ column += code.size + 1
54
+ column += 1 unless escape
55
+ when /\A([#\\]?[^#\\]*([#\\][^\\{#][^#\\]*)*)/
56
+ # Static text
57
+ text, string = $&, $'
58
+ text_lines = text.count("\n")
59
+
60
+ block << Sexp.new(:static, text, start: [line, column], finish: [(line + text_lines), (text_lines == 0 ? column + text.size : 1)])
61
+
62
+ line += text_lines
63
+ column = (text_lines == 0 ? column + text.size : 1)
64
+ end
65
+
66
+ break if string.empty?
67
+ end
68
+
69
+ block
70
+ end
71
+ end
72
+ end
73
+ end