haml_lint 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/haml-lint +7 -0
- data/config/default.yml +91 -0
- data/lib/haml_lint/cli.rb +122 -0
- data/lib/haml_lint/configuration.rb +97 -0
- data/lib/haml_lint/configuration_loader.rb +68 -0
- data/lib/haml_lint/constants.rb +8 -0
- data/lib/haml_lint/exceptions.rb +15 -0
- data/lib/haml_lint/file_finder.rb +69 -0
- data/lib/haml_lint/haml_visitor.rb +36 -0
- data/lib/haml_lint/lint.rb +25 -0
- data/lib/haml_lint/linter/alt_text.rb +12 -0
- data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -0
- data/lib/haml_lint/linter/classes_before_ids.rb +26 -0
- data/lib/haml_lint/linter/consecutive_comments.rb +20 -0
- data/lib/haml_lint/linter/consecutive_silent_scripts.rb +23 -0
- data/lib/haml_lint/linter/empty_script.rb +12 -0
- data/lib/haml_lint/linter/html_attributes.rb +14 -0
- data/lib/haml_lint/linter/implicit_div.rb +20 -0
- data/lib/haml_lint/linter/leading_comment_space.rb +14 -0
- data/lib/haml_lint/linter/line_length.rb +19 -0
- data/lib/haml_lint/linter/multiline_pipe.rb +43 -0
- data/lib/haml_lint/linter/multiline_script.rb +43 -0
- data/lib/haml_lint/linter/object_reference_attributes.rb +14 -0
- data/lib/haml_lint/linter/rubocop.rb +76 -0
- data/lib/haml_lint/linter/ruby_comments.rb +18 -0
- data/lib/haml_lint/linter/space_before_script.rb +52 -0
- data/lib/haml_lint/linter/space_inside_hash_attributes.rb +32 -0
- data/lib/haml_lint/linter/tag_name.rb +13 -0
- data/lib/haml_lint/linter/trailing_whitespace.rb +16 -0
- data/lib/haml_lint/linter/unnecessary_interpolation.rb +29 -0
- data/lib/haml_lint/linter/unnecessary_string_output.rb +39 -0
- data/lib/haml_lint/linter.rb +156 -0
- data/lib/haml_lint/linter_registry.rb +26 -0
- data/lib/haml_lint/logger.rb +107 -0
- data/lib/haml_lint/node_transformer.rb +28 -0
- data/lib/haml_lint/options.rb +89 -0
- data/lib/haml_lint/parser.rb +87 -0
- data/lib/haml_lint/rake_task.rb +107 -0
- data/lib/haml_lint/report.rb +16 -0
- data/lib/haml_lint/reporter/default_reporter.rb +39 -0
- data/lib/haml_lint/reporter/json_reporter.rb +44 -0
- data/lib/haml_lint/reporter.rb +36 -0
- data/lib/haml_lint/ruby_parser.rb +29 -0
- data/lib/haml_lint/runner.rb +76 -0
- data/lib/haml_lint/script_extractor.rb +181 -0
- data/lib/haml_lint/tree/comment_node.rb +5 -0
- data/lib/haml_lint/tree/doctype_node.rb +5 -0
- data/lib/haml_lint/tree/filter_node.rb +9 -0
- data/lib/haml_lint/tree/haml_comment_node.rb +18 -0
- data/lib/haml_lint/tree/node.rb +98 -0
- data/lib/haml_lint/tree/plain_node.rb +5 -0
- data/lib/haml_lint/tree/root_node.rb +5 -0
- data/lib/haml_lint/tree/script_node.rb +11 -0
- data/lib/haml_lint/tree/silent_script_node.rb +12 -0
- data/lib/haml_lint/tree/tag_node.rb +221 -0
- data/lib/haml_lint/utils.rb +58 -0
- data/lib/haml_lint/version.rb +4 -0
- data/lib/haml_lint.rb +36 -0
- 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,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,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
|
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
|