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,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