haml_lint 0.13.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/bin/haml-lint +7 -0
  3. data/config/default.yml +91 -0
  4. data/lib/haml_lint/cli.rb +122 -0
  5. data/lib/haml_lint/configuration.rb +97 -0
  6. data/lib/haml_lint/configuration_loader.rb +68 -0
  7. data/lib/haml_lint/constants.rb +8 -0
  8. data/lib/haml_lint/exceptions.rb +15 -0
  9. data/lib/haml_lint/file_finder.rb +69 -0
  10. data/lib/haml_lint/haml_visitor.rb +36 -0
  11. data/lib/haml_lint/lint.rb +25 -0
  12. data/lib/haml_lint/linter/alt_text.rb +12 -0
  13. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -0
  14. data/lib/haml_lint/linter/classes_before_ids.rb +26 -0
  15. data/lib/haml_lint/linter/consecutive_comments.rb +20 -0
  16. data/lib/haml_lint/linter/consecutive_silent_scripts.rb +23 -0
  17. data/lib/haml_lint/linter/empty_script.rb +12 -0
  18. data/lib/haml_lint/linter/html_attributes.rb +14 -0
  19. data/lib/haml_lint/linter/implicit_div.rb +20 -0
  20. data/lib/haml_lint/linter/leading_comment_space.rb +14 -0
  21. data/lib/haml_lint/linter/line_length.rb +19 -0
  22. data/lib/haml_lint/linter/multiline_pipe.rb +43 -0
  23. data/lib/haml_lint/linter/multiline_script.rb +43 -0
  24. data/lib/haml_lint/linter/object_reference_attributes.rb +14 -0
  25. data/lib/haml_lint/linter/rubocop.rb +76 -0
  26. data/lib/haml_lint/linter/ruby_comments.rb +18 -0
  27. data/lib/haml_lint/linter/space_before_script.rb +52 -0
  28. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +32 -0
  29. data/lib/haml_lint/linter/tag_name.rb +13 -0
  30. data/lib/haml_lint/linter/trailing_whitespace.rb +16 -0
  31. data/lib/haml_lint/linter/unnecessary_interpolation.rb +29 -0
  32. data/lib/haml_lint/linter/unnecessary_string_output.rb +39 -0
  33. data/lib/haml_lint/linter.rb +156 -0
  34. data/lib/haml_lint/linter_registry.rb +26 -0
  35. data/lib/haml_lint/logger.rb +107 -0
  36. data/lib/haml_lint/node_transformer.rb +28 -0
  37. data/lib/haml_lint/options.rb +89 -0
  38. data/lib/haml_lint/parser.rb +87 -0
  39. data/lib/haml_lint/rake_task.rb +107 -0
  40. data/lib/haml_lint/report.rb +16 -0
  41. data/lib/haml_lint/reporter/default_reporter.rb +39 -0
  42. data/lib/haml_lint/reporter/json_reporter.rb +44 -0
  43. data/lib/haml_lint/reporter.rb +36 -0
  44. data/lib/haml_lint/ruby_parser.rb +29 -0
  45. data/lib/haml_lint/runner.rb +76 -0
  46. data/lib/haml_lint/script_extractor.rb +181 -0
  47. data/lib/haml_lint/tree/comment_node.rb +5 -0
  48. data/lib/haml_lint/tree/doctype_node.rb +5 -0
  49. data/lib/haml_lint/tree/filter_node.rb +9 -0
  50. data/lib/haml_lint/tree/haml_comment_node.rb +18 -0
  51. data/lib/haml_lint/tree/node.rb +98 -0
  52. data/lib/haml_lint/tree/plain_node.rb +5 -0
  53. data/lib/haml_lint/tree/root_node.rb +5 -0
  54. data/lib/haml_lint/tree/script_node.rb +11 -0
  55. data/lib/haml_lint/tree/silent_script_node.rb +12 -0
  56. data/lib/haml_lint/tree/tag_node.rb +221 -0
  57. data/lib/haml_lint/utils.rb +58 -0
  58. data/lib/haml_lint/version.rb +4 -0
  59. data/lib/haml_lint.rb +36 -0
  60. metadata +175 -0
@@ -0,0 +1,181 @@
1
+ module HamlLint
2
+ # Utility class for extracting Ruby script from a HAML file that can then be
3
+ # linted with a Ruby linter (i.e. is "legal" Ruby). The goal is to turn this:
4
+ #
5
+ # - if signed_in?(viewer)
6
+ # %span Stuff
7
+ # = link_to 'Sign Out', sign_out_path
8
+ # - else
9
+ # .some-class{ class: my_method }= my_method
10
+ # = link_to 'Sign In', sign_in_path
11
+ #
12
+ # into this:
13
+ #
14
+ # if signed_in?(viewer)
15
+ # link_to 'Sign Out', sign_out_path
16
+ # else
17
+ # { class: my_method }
18
+ # my_method
19
+ # link_to 'Sign In', sign_in_path
20
+ # end
21
+ #
22
+ class ScriptExtractor
23
+ include HamlVisitor
24
+
25
+ attr_reader :source, :source_map
26
+
27
+ def initialize(parser)
28
+ @parser = parser
29
+ end
30
+
31
+ def extract
32
+ visit(@parser.tree)
33
+ @source = @code.join("\n")
34
+ end
35
+
36
+ def visit_root(_node)
37
+ @code = []
38
+ @total_lines = 0
39
+ @source_map = {}
40
+ @indent_level = 0
41
+
42
+ yield # Collect lines of code from children
43
+ end
44
+
45
+ def visit_plain(node)
46
+ # Don't output the text, as we don't want to have to deal with any RuboCop
47
+ # cops regarding StringQuotes or AsciiComments, and it's not important to
48
+ # overall document anyway.
49
+ add_line('puts', node)
50
+ end
51
+
52
+ def visit_tag(node)
53
+ additional_attributes = node.dynamic_attributes_sources
54
+
55
+ # Include dummy references to code executed in attributes list
56
+ # (this forces a "use" of a variable to prevent "assigned but unused
57
+ # variable" lints)
58
+ additional_attributes.each do |attributes_code|
59
+ # Normalize by removing excess whitespace to avoid format lints
60
+ attributes_code = attributes_code.gsub(/\s*\n\s*/, ' ').strip
61
+
62
+ # Attributes can either be a method call or a literal hash, so wrap it
63
+ # in a method call itself in order to avoid having to differentiate the
64
+ # two.
65
+ add_line("{}.merge(#{attributes_code.strip})", node)
66
+ end
67
+
68
+ check_tag_static_hash_source(node)
69
+
70
+ # We add a dummy puts statement to represent the tag name being output.
71
+ # This prevents some erroneous RuboCop warnings.
72
+ add_line("puts # #{node.tag_name}", node)
73
+
74
+ code = node.script.strip
75
+ add_line(code, node) unless code.empty?
76
+ end
77
+
78
+ def visit_script(node)
79
+ code = node.text
80
+ add_line(code.strip, node)
81
+
82
+ start_block = anonymous_block?(code) || start_block_keyword?(code)
83
+
84
+ if start_block
85
+ @indent_level += 1
86
+ end
87
+
88
+ yield # Continue extracting code from children
89
+
90
+ if start_block
91
+ @indent_level -= 1
92
+ add_line('end', node)
93
+ end
94
+ end
95
+
96
+ def visit_silent_script(node, &block)
97
+ visit_script(node, &block)
98
+ end
99
+
100
+ def visit_filter(node)
101
+ if node.filter_type == 'ruby'
102
+ node.text.split("\n").each_with_index do |line, index|
103
+ add_line(line, node.line + index + 1)
104
+ end
105
+ else
106
+ add_line('puts', node)
107
+ HamlLint::Utils.extract_interpolated_values(node.text) do |interpolated_code|
108
+ add_line(interpolated_code, node)
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def check_tag_static_hash_source(node)
116
+ # Haml::Parser converts hashrocket-style hash attributes of strings and symbols
117
+ # to static attributes, and excludes them from the dynamic attribute sources:
118
+ # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L400-L404
119
+ # https://github.com/haml/haml/blob/08f97ec4dc8f59fe3d7f6ab8f8807f86f2a15b68/lib/haml/parser.rb#L540-L554
120
+ # Here, we add the hash source back in so it can be inspected by rubocop.
121
+ if node.hash_attributes? && node.dynamic_attributes_sources.empty?
122
+ normalized_attr_source = node.dynamic_attributes_source[:hash].gsub(/\s*\n\s*/, ' ')
123
+
124
+ add_line(normalized_attr_source, node)
125
+ end
126
+ end
127
+
128
+ def add_line(code, node_or_line)
129
+ return if code.empty?
130
+
131
+ indent_level = @indent_level
132
+
133
+ if node_or_line.respond_to?(:line)
134
+ # Since mid-block keywords are children of the corresponding start block
135
+ # keyword, we need to reduce their indentation level by 1. However, we
136
+ # don't do this unless this is an actual tag node (a raw line number
137
+ # means this came from a `:ruby` filter).
138
+ indent_level -= 1 if mid_block_keyword?(code)
139
+ end
140
+
141
+ indent = (' ' * 2 * indent_level)
142
+
143
+ @code << indent + code
144
+
145
+ original_line =
146
+ node_or_line.respond_to?(:line) ? node_or_line.line : node_or_line
147
+
148
+ # For interpolated code in filters that spans multiple lines, the
149
+ # resulting code will span multiple lines, so we need to create a
150
+ # mapping for each line.
151
+ (code.count("\n") + 1).times do
152
+ @total_lines += 1
153
+ @source_map[@total_lines] = original_line
154
+ end
155
+ end
156
+
157
+ def anonymous_block?(text)
158
+ text =~ /\bdo\s*(\|\s*[^\|]*\s*\|)?\z/
159
+ end
160
+
161
+ START_BLOCK_KEYWORDS = %w[if unless case begin for until while]
162
+ def start_block_keyword?(text)
163
+ START_BLOCK_KEYWORDS.include?(block_keyword(text))
164
+ end
165
+
166
+ MID_BLOCK_KEYWORDS = %w[else elsif when rescue ensure]
167
+ def mid_block_keyword?(text)
168
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
169
+ end
170
+
171
+ def block_keyword(text)
172
+ # Need to handle 'for'/'while' since regex stolen from HAML parser doesn't
173
+ if keyword = text[/\A\s*([^\s]+)\s+/, 1]
174
+ return keyword if %w[for until while].include?(keyword)
175
+ end
176
+
177
+ return unless keyword = text.scan(Haml::Parser::BLOCK_KEYWORD_REGEX)[0]
178
+ keyword[0] || keyword[1]
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,5 @@
1
+ module HamlLint::Tree
2
+ # Represents a visible XHTML comment in a HAML document.
3
+ class CommentNode < Node
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module HamlLint::Tree
2
+ # Represents a doctype definition for a HAML document.
3
+ class DoctypeNode < Node
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module HamlLint::Tree
2
+ # Represents a filter node which contains arbitrary code.
3
+ class FilterNode < Node
4
+ # The type of code contained in this filter.
5
+ def filter_type
6
+ @value[:name]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module HamlLint::Tree
2
+ # Represents a HAML comment node.
3
+ class HamlCommentNode < Node
4
+ # Returns the full text content of this comment, including newlines if a
5
+ # single comment spans multiple lines.
6
+ #
7
+ # @return [String]
8
+ def text
9
+ content = source_code
10
+ indent = content[/^ */]
11
+
12
+ content.gsub(/^#{indent}/, '')
13
+ .gsub(/^-#/, '')
14
+ .gsub(/^ /, '')
15
+ .rstrip
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,98 @@
1
+ module HamlLint::Tree
2
+ # Decorator class that provides a convenient set of helpers for HAML's
3
+ # {Haml::Parser::ParseNode} struct.
4
+ #
5
+ # The goal is to abstract away the details of the underlying struct and
6
+ # provide a cleaner and more uniform interface for getting information about a
7
+ # node, as there are a number of weird/special cases in the struct returned by
8
+ # the HAML parser.
9
+ #
10
+ # @abstract
11
+ class Node
12
+ attr_accessor :children, :parent
13
+ attr_reader :line, :type
14
+
15
+ # Creates a node wrapping the given {Haml::Parser::ParseNode} struct.
16
+ #
17
+ # @param parser [HamlLint::Parser] parser that created this node
18
+ # @param parse_node [Haml::Parser::ParseNode] parse node created by HAML's parser
19
+ def initialize(parser, parse_node)
20
+ # TODO: Change signature to take source code object, not parser
21
+ @line = parse_node.line
22
+ @parser = parser
23
+ @value = parse_node.value
24
+ @type = parse_node.type
25
+ end
26
+
27
+ # Returns the first node found under the subtree which matches the given
28
+ # block.
29
+ #
30
+ # Returns nil if no node matching the given block was found.
31
+ #
32
+ # @return [HamlLint::Tree::Node,nil]
33
+ def find(&block)
34
+ return self if block.call(self)
35
+
36
+ children.each do |child|
37
+ if result = child.find(&block)
38
+ return result
39
+ end
40
+ end
41
+
42
+ nil # Otherwise no matching node was found
43
+ end
44
+
45
+ # Source code of all lines this node spans (excluding children).
46
+ #
47
+ # @return [String]
48
+ def source_code
49
+ next_node_line =
50
+ if next_node
51
+ next_node.line - 1
52
+ else
53
+ @parser.lines.count + 1
54
+ end
55
+
56
+ @parser.lines[@line - 1...next_node_line]
57
+ .join("\n")
58
+ .gsub(/^\s*\z/m, '') # Remove blank lines at the end
59
+ end
60
+
61
+ def inspect
62
+ "#<#{self.class.name}>"
63
+ end
64
+
65
+ # Returns the node that follows this node, whether it be a sibling or an
66
+ # ancestor's child, but not a child of this node.
67
+ #
68
+ # If you are also willing to return the child, call {#next_node}.
69
+ #
70
+ # Returns nil if there is no successor.
71
+ #
72
+ # @return [HamlLint::Tree::Node,nil]
73
+ def successor
74
+ siblings = parent ? parent.children : [self]
75
+
76
+ next_sibling = siblings[siblings.index(self) + 1] if siblings.count > 1
77
+ return next_sibling if next_sibling
78
+
79
+ parent.successor if parent
80
+ end
81
+
82
+ # Returns the next node that appears after this node in the document.
83
+ #
84
+ # Returns nil if there is no next node.
85
+ #
86
+ # @return [HamlLint::Tree::Node,nil]
87
+ def next_node
88
+ children.first || successor
89
+ end
90
+
91
+ # Returns the text content of this node.
92
+ #
93
+ # @return [String]
94
+ def text
95
+ @value[:text].to_s
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ module HamlLint::Tree
2
+ # Represents a node that contains plain text.
3
+ class PlainNode < Node
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module HamlLint::Tree
2
+ # Represents the root node of a HAML document that contains all other nodes.
3
+ class RootNode < Node
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module HamlLint::Tree
2
+ # Represents a node which produces output based on Ruby code.
3
+ class ScriptNode < Node
4
+ # Returns the source for the script following the `-` marker.
5
+ #
6
+ # @return [String]
7
+ def script
8
+ @value[:text]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module HamlLint::Tree
2
+ # Represents a HAML silent script node (`- some_expression`) which executes
3
+ # code without producing output.
4
+ class SilentScriptNode < Node
5
+ # Returns the source for the script following the `-` marker.
6
+ #
7
+ # @return [String]
8
+ def script
9
+ @value[:text]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,221 @@
1
+ module HamlLint::Tree
2
+ # Represents a tag node in a HAML document.
3
+ class TagNode < Node
4
+ # Computed set of attribute hashes code.
5
+ #
6
+ # This is a combination of all dynamically calculated attributes from the
7
+ # different attribute setting syntaxes (`{...}`/`(...)`), converted into
8
+ # Ruby code.
9
+ #
10
+ # @return [Array<String>]
11
+ def dynamic_attributes_sources
12
+ @value[:attributes_hashes]
13
+ end
14
+
15
+ # Returns whether this tag contains executable script (e.g. is followed by a
16
+ # `=`).
17
+ #
18
+ # @return [true,false]
19
+ def contains_script?
20
+ @value[:parse] && !@value[:value].strip.empty?
21
+ end
22
+
23
+ # Returns whether this tag has a specified attribute.
24
+ #
25
+ # @return [true,false]
26
+ def has_hash_attribute?(attribute)
27
+ hash_attributes? && existing_attributes.include?(attribute)
28
+ end
29
+
30
+ # List of classes statically defined for this tag.
31
+ #
32
+ # @example For `%tag.button.button-info{ class: status }`, this returns:
33
+ # ['button', 'button-info']
34
+ #
35
+ # @return [Array<String>] list of statically defined classes with leading
36
+ # dot removed
37
+ def static_classes
38
+ @static_classes ||=
39
+ begin
40
+ static_attributes_source.scan(/\.([-:\w]+)/)
41
+ end
42
+ end
43
+
44
+ # List of ids statically defined for this tag.
45
+ #
46
+ # @example For `%tag.button#start-button{ id: special_id }`, this returns:
47
+ # ['start-button']
48
+ #
49
+ # @return [Array<String>] list of statically defined ids with leading `#`
50
+ # removed
51
+ def static_ids
52
+ @static_ids ||=
53
+ begin
54
+ static_attributes_source.scan(/#([-:\w]+)/)
55
+ end
56
+ end
57
+
58
+ # Static element attributes defined after the tag name.
59
+ #
60
+ # @example For `%tag.button#start-button`, this returns:
61
+ # '.button#start-button'
62
+ #
63
+ # @return [String]
64
+ def static_attributes_source
65
+ attributes_source[:static] || ''
66
+ end
67
+
68
+ # Returns the source code for the dynamic attributes defined in `{...}`,
69
+ # `(...)`, or `[...]` after a tag name.
70
+ #
71
+ # @example For `%tag.class{ id: 'hello' }(lang=en)`, this returns:
72
+ # { :hash => " id: 'hello' ", :html => "lang=en" }
73
+ #
74
+ # @return [Hash]
75
+ def dynamic_attributes_source
76
+ @dynamic_attributes_source ||=
77
+ attributes_source.reject { |key| key == :static }
78
+ end
79
+
80
+ # Returns the source code for the static and dynamic attributes
81
+ # of a tag.
82
+ #
83
+ # @example For `%tag.class{ id: 'hello' }(lang=en)`, this returns:
84
+ # { :static => '.class', :hash => " id: 'hello' ", :html => "lang=en" }
85
+ #
86
+ # @return [Hash]
87
+ def attributes_source
88
+ @attr_source ||=
89
+ begin
90
+ _explicit_tag, static_attrs, rest = source_code
91
+ .scan(/\A\s*(%[-:\w]+)?([-:\w\.\#]*)(.*)/m)[0]
92
+
93
+ attr_types = {
94
+ '{' => [:hash, %w[{ }]],
95
+ '(' => [:html, %w[( )]],
96
+ '[' => [:object_ref, %w[[ ]]],
97
+ }
98
+
99
+ attr_source = { static: static_attrs }
100
+ while rest
101
+ type, chars = attr_types[rest[0]]
102
+ break unless type # Not an attribute opening character, so we're done
103
+
104
+ # Can't define multiple of the same attribute type (e.g. two {...})
105
+ break if attr_source[type]
106
+
107
+ attr_source[type], rest = Haml::Util.balance(rest, *chars)
108
+ end
109
+
110
+ attr_source
111
+ end
112
+ end
113
+
114
+ # Whether this tag node has a set of hash attributes defined via the
115
+ # curly brace syntax (e.g. `%tag{ lang: 'en' }`).
116
+ #
117
+ # @return [true,false]
118
+ def hash_attributes?
119
+ !dynamic_attributes_source[:hash].nil?
120
+ end
121
+
122
+ # Attributes defined after the tag name in Ruby hash brackets (`{}`).
123
+ #
124
+ # @example For `%tag.class{ lang: 'en' }`, this returns:
125
+ # " lang: 'en' "
126
+ #
127
+ # @return [String] source without the surrounding curly braces
128
+ def hash_attributes_source
129
+ dynamic_attributes_source[:hash]
130
+ end
131
+
132
+ # Whether this tag node has a set of HTML attributes defined via the
133
+ # parentheses syntax (e.g. `%tag(lang=en)`).
134
+ #
135
+ # @return [true,false]
136
+ def html_attributes?
137
+ !dynamic_attributes_source[:html].nil?
138
+ end
139
+
140
+ # Attributes defined after the tag name in parentheses (`()`).
141
+ #
142
+ # @example For `%tag.class(lang=en)`, this returns:
143
+ # "lang=en"
144
+ #
145
+ # @return [String] source without the surrounding parentheses
146
+ def html_attributes_source
147
+ dynamic_attributes_source[:html] || ''
148
+ end
149
+
150
+ # Name of the HTML tag.
151
+ #
152
+ # @return [String]
153
+ def tag_name
154
+ @value[:name]
155
+ end
156
+
157
+ # Whether this tag node has a set of square brackets (e.g. `%tag[...]`)
158
+ # following it that indicates its class and ID will be to the value of the
159
+ # given object's {#to_key} or {#id} method (in that order).
160
+ #
161
+ # @return [true,false]
162
+ def object_reference?
163
+ @value[:object_ref].to_s != 'nil'
164
+ end
165
+
166
+ # Source code for the contents of the node's object reference.
167
+ #
168
+ # @see http://haml.info/docs/yardoc/file.REFERENCE.html#object_reference_
169
+ # @return [String,nil] string source of object reference or `nil` if it has
170
+ # not been defined
171
+ def object_reference_source
172
+ (@value[:object_ref] if object_reference?) || ''
173
+ end
174
+
175
+ # Whether this node had a `<` after it signifying that outer whitespace
176
+ # should be removed.
177
+ #
178
+ # @return [true,false]
179
+ def remove_inner_whitespace?
180
+ @value[:nuke_inner_whitespace]
181
+ end
182
+
183
+ # Whether this node had a `>` after it signifying that outer whitespace
184
+ # should be removed.
185
+ #
186
+ # @return [true,false]
187
+ def remove_outer_whitespace?
188
+ @value[:nuke_inner_whitespace]
189
+ end
190
+
191
+ # Returns the script source that will be evaluated to produce this tag's
192
+ # inner content, if any.
193
+ #
194
+ # @return [String]
195
+ def script
196
+ (@value[:value] if @value[:parse]) || ''
197
+ end
198
+
199
+ # Returns the static inner content for this tag.
200
+ #
201
+ # If this tag contains dynamic content of any kind, this will still return
202
+ # an empty string, and you'll have to use {#script} to obtain the source.
203
+ #
204
+ # @return [String]
205
+ def text
206
+ (@value[:value] if @value[:parse]) || ''
207
+ end
208
+
209
+ private
210
+
211
+ def parsed_attributes
212
+ HamlLint::RubyParser.new.parse(hash_attributes_source)
213
+ end
214
+
215
+ def existing_attributes
216
+ parsed_attributes.children.collect do |parsed_attribute|
217
+ parsed_attribute.children.first.to_a.first
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,58 @@
1
+ module HamlLint
2
+ # A miscellaneous set of utility functions.
3
+ module Utils
4
+ module_function
5
+
6
+ # Yields interpolated values within a block of filter text.
7
+ def extract_interpolated_values(filter_text)
8
+ Haml::Util.handle_interpolation(filter_text.dump) do |scan|
9
+ escape_count = (scan[2].size - 1) / 2
10
+ return unless escape_count.even? # rubocop:disable Lint/NonLocalExitFromIterator
11
+
12
+ dumped_interpolated_str = Haml::Util.balance(scan, '{', '}', 1)[0][0...-1]
13
+
14
+ # Hacky way to turn a dumped string back into a regular string
15
+ yield eval('"' + dumped_interpolated_str + '"') # rubocop:disable Eval
16
+ end
17
+ end
18
+
19
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
20
+ def camel_case(str)
21
+ str.split(/_|-| /).map { |part| part.sub(/^\w/) { |c| c.upcase } }.join
22
+ end
23
+
24
+ # Find all consecutive nodes satisfying the given {Proc} of a minimum size
25
+ # and yield each group.
26
+ #
27
+ # @param items [Array]
28
+ # @param min_size [Fixnum] minimum number of consecutive items before
29
+ # yielding
30
+ # @param satisfies [Proc] function that takes an item and returns true/false
31
+ def find_consecutive(items, min_size, satisfies)
32
+ current = -1
33
+
34
+ while (current += 1) < items.count
35
+ next unless satisfies[items[current]]
36
+
37
+ count = count_consecutive(items, current, satisfies)
38
+ next unless count >= min_size
39
+
40
+ # Yield the chunk of consecutive items
41
+ yield items[current...(current + count)]
42
+
43
+ current += count # Skip this patch of consecutive items to find more
44
+ end
45
+ end
46
+
47
+ # Count the number of consecutive items satisfying the given {Proc}.
48
+ #
49
+ # @param items [Array]
50
+ # @param offset [Fixnum] index to start searching
51
+ # @param satisfies [Proc] function to evaluate item with
52
+ def count_consecutive(items, offset, satisfies)
53
+ count = 1
54
+ count += 1 while (offset + count < items.count) && satisfies[items[offset + count]]
55
+ count
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module HamlLint
3
+ VERSION = '0.13.0'
4
+ end
data/lib/haml_lint.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'haml_lint/constants'
2
+ require 'haml_lint/exceptions'
3
+ require 'haml_lint/configuration'
4
+ require 'haml_lint/configuration_loader'
5
+ require 'haml_lint/parser'
6
+ require 'haml_lint/haml_visitor'
7
+ require 'haml_lint/lint'
8
+ require 'haml_lint/linter_registry'
9
+ require 'haml_lint/ruby_parser'
10
+ require 'haml_lint/linter'
11
+ require 'haml_lint/logger'
12
+ require 'haml_lint/reporter'
13
+ require 'haml_lint/report'
14
+ require 'haml_lint/file_finder'
15
+ require 'haml_lint/runner'
16
+ require 'haml_lint/utils'
17
+ require 'haml_lint/version'
18
+
19
+ require 'haml'
20
+
21
+ # Load all parse tree node classes
22
+ require 'haml_lint/tree/node'
23
+ require 'haml_lint/node_transformer'
24
+ Dir[File.expand_path('haml_lint/tree/*.rb', File.dirname(__FILE__))].each do |file|
25
+ require file
26
+ end
27
+
28
+ # Load all linters
29
+ Dir[File.expand_path('haml_lint/linter/*.rb', File.dirname(__FILE__))].each do |file|
30
+ require file
31
+ end
32
+
33
+ # Load all reporters
34
+ Dir[File.expand_path('haml_lint/reporter/*.rb', File.dirname(__FILE__))].each do |file|
35
+ require file
36
+ end