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.
- 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,14 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for the setting of attributes via HTML shorthand syntax on elements
|
3
|
+
# (e.g. `%tag(lang=en)`).
|
4
|
+
class Linter::HtmlAttributes < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
def visit_tag(node)
|
8
|
+
return unless node.html_attributes?
|
9
|
+
|
10
|
+
add_lint(node, "Prefer the hash attributes syntax (%tag{ lang: 'en' }) over " \
|
11
|
+
'HTML attributes syntax (%tag(lang=en))')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for unnecessary uses of the `%div` prefix where a class name or ID
|
3
|
+
# already implies a div.
|
4
|
+
class Linter::ImplicitDiv < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
def visit_tag(node)
|
8
|
+
return unless node.tag_name == 'div'
|
9
|
+
|
10
|
+
return unless node.static_classes.any? || node.static_ids.any?
|
11
|
+
|
12
|
+
tag = node.source_code[/\s*([^\s={\(\[]+)/, 1]
|
13
|
+
return unless tag.start_with?('%div')
|
14
|
+
|
15
|
+
add_lint(node,
|
16
|
+
"`#{tag}` can be written as `#{node.static_attributes_source}` " \
|
17
|
+
'since `%div` is implicit')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for comments that don't have a leading space.
|
3
|
+
class Linter::LeadingCommentSpace < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_haml_comment(node)
|
7
|
+
# Skip if the node spans multiple lines starting on the second line,
|
8
|
+
# or starts with a space
|
9
|
+
return if node.text.match(/\A(\s*|\s+\S.*)$/)
|
10
|
+
|
11
|
+
add_lint(node, 'Comment should have a space after the `#`')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for lines longer than a maximum number of columns.
|
3
|
+
class Linter::LineLength < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
MSG = 'Line is too long. [%d/%d]'
|
7
|
+
|
8
|
+
def visit_root(_node)
|
9
|
+
max_length = config['max']
|
10
|
+
dummy_node = Struct.new(:line)
|
11
|
+
|
12
|
+
parser.lines.each_with_index do |line, index|
|
13
|
+
next if line.length <= max_length
|
14
|
+
|
15
|
+
add_lint(dummy_node.new(index + 1), format(MSG, line.length, max_length))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for uses of the multiline pipe character.
|
3
|
+
class Linter::MultilinePipe < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
MESSAGE = "Don't use the `|` character to split up lines. " \
|
7
|
+
'Wrap on commas or extract code into helper.'
|
8
|
+
|
9
|
+
def visit_tag(node)
|
10
|
+
check(node)
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_script(node)
|
14
|
+
check(node)
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit_silent_script(node)
|
18
|
+
check(node)
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_plain(node)
|
22
|
+
line = line_text_for_node(node)
|
23
|
+
|
24
|
+
# Plain text nodes are allowed to consist of a single pipe
|
25
|
+
return if line.strip == '|'
|
26
|
+
|
27
|
+
add_lint(node, MESSAGE) if line.match(MULTILINE_PIPE_REGEX)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
MULTILINE_PIPE_REGEX = /\s+\|\s*$/
|
33
|
+
|
34
|
+
def line_text_for_node(node)
|
35
|
+
parser.lines[node.line - 1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def check(node)
|
39
|
+
line = line_text_for_node(node)
|
40
|
+
add_lint(node, MESSAGE) if line.match(MULTILINE_PIPE_REGEX)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks scripts spread over multiple lines.
|
3
|
+
class Linter::MultilineScript < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
# List of operators that can split a script into two lines that we want to
|
7
|
+
# alert on.
|
8
|
+
SPLIT_OPERATORS = %w[
|
9
|
+
|| or && and
|
10
|
+
||= &&=
|
11
|
+
^ << >> | &
|
12
|
+
<<= >>= |= &=
|
13
|
+
+ - * / ** %
|
14
|
+
+= -= *= /= **= %=
|
15
|
+
< <= <=> >= >
|
16
|
+
= == === != =~ !~
|
17
|
+
.. ...
|
18
|
+
? :
|
19
|
+
not
|
20
|
+
if unless while until
|
21
|
+
begin
|
22
|
+
].to_set
|
23
|
+
|
24
|
+
def visit_script(node)
|
25
|
+
check(node)
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_silent_script(node)
|
29
|
+
check(node)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def check(node)
|
35
|
+
operator = node.script[/\s+(\S+)\z/, 1]
|
36
|
+
if SPLIT_OPERATORS.include?(operator)
|
37
|
+
add_lint(node,
|
38
|
+
"Script with trailing operator `#{operator}` should be " \
|
39
|
+
'merged with the script on the following line')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for uses of the object reference syntax for assigning the class and
|
3
|
+
# ID attributes for an element (e.g. `%div[@user]`).
|
4
|
+
class Linter::ObjectReferenceAttributes < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
def visit_tag(node)
|
8
|
+
return unless node.object_reference?
|
9
|
+
|
10
|
+
add_lint(node, 'Avoid using object reference syntax to assign class/id ' \
|
11
|
+
'attributes for tags')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'haml_lint/script_extractor'
|
2
|
+
require 'rubocop'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module HamlLint
|
6
|
+
# Runs RuboCop on Ruby code contained within HAML templates.
|
7
|
+
class Linter::RuboCop < Linter
|
8
|
+
include LinterRegistry
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
super
|
12
|
+
@rubocop = ::RuboCop::CLI.new
|
13
|
+
@ignored_cops = Array(config['ignored_cops']).flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(parser)
|
17
|
+
@parser = parser
|
18
|
+
@extractor = ScriptExtractor.new(parser)
|
19
|
+
extracted_code = @extractor.extract.strip
|
20
|
+
|
21
|
+
# Ensure a final newline in the code we feed to RuboCop
|
22
|
+
find_lints(extracted_code + "\n") unless extracted_code.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_lints(code)
|
28
|
+
original_filename = @parser.filename || 'ruby_script'
|
29
|
+
filename = "#{File.basename(original_filename)}.haml_lint.tmp"
|
30
|
+
directory = File.dirname(original_filename)
|
31
|
+
|
32
|
+
Tempfile.open(filename, directory) do |f|
|
33
|
+
begin
|
34
|
+
f.write(code)
|
35
|
+
f.close
|
36
|
+
extract_lints_from_offences(lint_file(f.path))
|
37
|
+
ensure
|
38
|
+
f.unlink
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Defined so we can stub the results in tests
|
44
|
+
def lint_file(file)
|
45
|
+
@rubocop.run(%w[--format HamlLint::OffenceCollector] << file)
|
46
|
+
OffenceCollector.offences
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_lints_from_offences(offences)
|
50
|
+
offences.select { |offence| !@ignored_cops.include?(offence.cop_name) }
|
51
|
+
.each do |offence|
|
52
|
+
@lints << Lint.new(self,
|
53
|
+
@parser.filename,
|
54
|
+
@extractor.source_map[offence.line],
|
55
|
+
"#{offence.cop_name}: #{offence.message}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Collects offences detected by RuboCop.
|
61
|
+
class OffenceCollector < ::RuboCop::Formatter::BaseFormatter
|
62
|
+
attr_accessor :offences
|
63
|
+
|
64
|
+
class << self
|
65
|
+
attr_accessor :offences
|
66
|
+
end
|
67
|
+
|
68
|
+
def started(_target_files)
|
69
|
+
self.class.offences = []
|
70
|
+
end
|
71
|
+
|
72
|
+
def file_finished(_file, offences)
|
73
|
+
self.class.offences += offences
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for Ruby comments that can be written as HAML comments.
|
3
|
+
class Linter::RubyComments < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_silent_script(node)
|
7
|
+
if code_comment?(node)
|
8
|
+
add_lint(node, 'Use `-#` for comments instead of `- #`')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def code_comment?(node)
|
15
|
+
node.script =~ /\A\s+#/
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for Ruby script in HAML templates with no space after the `=`/`-`.
|
3
|
+
class Linter::SpaceBeforeScript < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
MESSAGE_FORMAT = 'The %s symbol should have one space separating it from code'
|
7
|
+
|
8
|
+
def visit_tag(node) # rubocop:disable Metrics/CyclomaticComplexity
|
9
|
+
# If this tag has inline script
|
10
|
+
return unless node.contains_script?
|
11
|
+
|
12
|
+
text = node.script.strip
|
13
|
+
return if text.empty?
|
14
|
+
|
15
|
+
tag_with_text = tag_with_inline_text(node)
|
16
|
+
|
17
|
+
unless index = tag_with_text.rindex(text)
|
18
|
+
# For tags with inline text that contain interpolation, the parser
|
19
|
+
# converts them to inline script by surrounding them in string quotes,
|
20
|
+
# e.g. `%p Hello #{name}` becomes `%p= "Hello #{name}"`, causing the
|
21
|
+
# above search to fail. Check for this case by removing added quotes.
|
22
|
+
if text_without_quotes = strip_surrounding_quotes(text)
|
23
|
+
return unless index = tag_with_text.rindex(text_without_quotes)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if the character before the start of the script is a space
|
28
|
+
# (need to do it this way as the parser strips whitespace from node)
|
29
|
+
return unless tag_with_text[index - 1] != ' '
|
30
|
+
|
31
|
+
add_lint(node, MESSAGE_FORMAT % '=')
|
32
|
+
end
|
33
|
+
|
34
|
+
def visit_script(node)
|
35
|
+
# Plain text nodes with interpolation are converted to script nodes, so we
|
36
|
+
# need to ignore them here.
|
37
|
+
return unless parser.lines[node.line - 1].lstrip.start_with?('=')
|
38
|
+
add_lint(node, MESSAGE_FORMAT % '=') if missing_space?(node)
|
39
|
+
end
|
40
|
+
|
41
|
+
def visit_silent_script(node)
|
42
|
+
add_lint(node, MESSAGE_FORMAT % '-') if missing_space?(node)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def missing_space?(node)
|
48
|
+
text = node.script
|
49
|
+
text[0] != ' ' if text
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for spaces inside the braces of hash attributes
|
3
|
+
# (e.g. `%tag{ lang: en }` vs `%tag{lang: en}`).
|
4
|
+
class Linter::SpaceInsideHashAttributes < Linter
|
5
|
+
include LinterRegistry
|
6
|
+
|
7
|
+
STYLE = {
|
8
|
+
'no_space' => {
|
9
|
+
start_regex: /\A\{[^ ]/,
|
10
|
+
end_regex: /[^ ]\}\z/,
|
11
|
+
start_message: 'Hash attribute should start with no space after the opening brace',
|
12
|
+
end_message: 'Hash attribute should end with no space before the closing brace'
|
13
|
+
},
|
14
|
+
'space' => {
|
15
|
+
start_regex: /\A\{ [^ ]/,
|
16
|
+
end_regex: /[^ ] \}\z/,
|
17
|
+
start_message: 'Hash attribute should start with one space after the opening brace',
|
18
|
+
end_message: 'Hash attribute should end with one space before the closing brace'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
def visit_tag(node)
|
23
|
+
return unless node.hash_attributes?
|
24
|
+
|
25
|
+
style = STYLE[config['style'] == 'no_space' ? 'no_space' : 'space']
|
26
|
+
source = node.hash_attributes_source
|
27
|
+
|
28
|
+
add_lint(node, style[:start_message]) unless source =~ style[:start_regex]
|
29
|
+
add_lint(node, style[:end_message]) unless source =~ style[:end_regex]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for tag names with uppercase letters.
|
3
|
+
class Linter::TagName < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_tag(node)
|
7
|
+
tag = node.tag_name
|
8
|
+
return unless tag.match(/[A-Z]/)
|
9
|
+
|
10
|
+
add_lint(node, "`#{tag}` should be written in lowercase as `#{tag.downcase}`")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for trailing whitespace.
|
3
|
+
class Linter::TrailingWhitespace < Linter
|
4
|
+
include LinterRegistry
|
5
|
+
|
6
|
+
def visit_root(_node)
|
7
|
+
dummy_node = Struct.new(:line)
|
8
|
+
|
9
|
+
parser.lines.each_with_index do |line, index|
|
10
|
+
next unless line =~ /\s+$/
|
11
|
+
|
12
|
+
add_lint dummy_node.new(index + 1), 'Line contains trailing whitespace'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for unnecessary uses of string interpolation.
|
3
|
+
#
|
4
|
+
# For example, the following two code snippets are equivalent, but the latter
|
5
|
+
# is more concise (and thus preferred):
|
6
|
+
#
|
7
|
+
# %tag #{expression}
|
8
|
+
# %tag= expression
|
9
|
+
class Linter::UnnecessaryInterpolation < Linter
|
10
|
+
include LinterRegistry
|
11
|
+
|
12
|
+
def visit_tag(node)
|
13
|
+
return if node.script.empty?
|
14
|
+
|
15
|
+
count = 0
|
16
|
+
chars = 2 # Include surrounding quote chars
|
17
|
+
HamlLint::Utils.extract_interpolated_values(node.script) do |interpolated_code|
|
18
|
+
count += 1
|
19
|
+
return if count > 1 # rubocop:disable Lint/NonLocalExitFromIterator
|
20
|
+
chars += interpolated_code.length + 3
|
21
|
+
end
|
22
|
+
|
23
|
+
if chars == node.script.length
|
24
|
+
add_lint(node, '`%... \#{expression}` can be written without ' \
|
25
|
+
'interpolation as `%...= expression`')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Checks for unnecessary outputting of strings in Ruby script tags.
|
3
|
+
#
|
4
|
+
# For example, the following two code snippets are equivalent, but the latter
|
5
|
+
# is more concise (and thus preferred):
|
6
|
+
#
|
7
|
+
# %tag= "Some #{expression}"
|
8
|
+
# %tag Some #{expression}
|
9
|
+
class Linter::UnnecessaryStringOutput < Linter
|
10
|
+
include LinterRegistry
|
11
|
+
|
12
|
+
MESSAGE = '`= "..."` should be rewritten as `...`'
|
13
|
+
|
14
|
+
def visit_tag(node)
|
15
|
+
if tag_has_inline_script?(node) && inline_content_is_string?(node)
|
16
|
+
add_lint(node, MESSAGE)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_script(node)
|
21
|
+
# Some script nodes created by the HAML parser aren't actually script
|
22
|
+
# nodes declared via the `=` marker. Check for it.
|
23
|
+
return if node.source_code !~ /\s*=/
|
24
|
+
|
25
|
+
if outputs_string_literal?(node)
|
26
|
+
add_lint(node, MESSAGE)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def outputs_string_literal?(script_node)
|
33
|
+
tree = parse_ruby(script_node.script)
|
34
|
+
[:str, :dstr].include?(tree.type)
|
35
|
+
rescue ::Parser::SyntaxError # rubocop:disable Lint/HandleExceptions
|
36
|
+
# Gracefully ignore syntax errors, as that's managed by a different linter
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module HamlLint
|
2
|
+
# Base implementation for all lint checks.
|
3
|
+
class Linter
|
4
|
+
include HamlVisitor
|
5
|
+
|
6
|
+
attr_reader :parser, :lints
|
7
|
+
|
8
|
+
# @param config [Hash] configuration for this linter
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
@lints = []
|
12
|
+
@ruby_parser = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(parser)
|
16
|
+
@parser = parser
|
17
|
+
visit(parser.tree)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the simple name for this linter.
|
21
|
+
def name
|
22
|
+
self.class.name.split('::').last
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :config
|
28
|
+
|
29
|
+
def add_lint(node, message)
|
30
|
+
@lints << Lint.new(self, parser.filename, node.line, message)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parse Ruby code into an abstract syntax tree.
|
34
|
+
#
|
35
|
+
# @return [AST::Node]
|
36
|
+
def parse_ruby(source)
|
37
|
+
@ruby_parser ||= HamlLint::RubyParser.new
|
38
|
+
@ruby_parser.parse(source)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove the surrounding double quotes from a string, ignoring any
|
42
|
+
# leading/trailing whitespace.
|
43
|
+
#
|
44
|
+
# @param string [String]
|
45
|
+
# @return [String] stripped with leading/trailing double quotes removed.
|
46
|
+
def strip_surrounding_quotes(string)
|
47
|
+
string[/\A\s*"(.*)"\s*\z/, 1]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns whether a string contains any interpolation.
|
51
|
+
#
|
52
|
+
# @param string [String]
|
53
|
+
# @return [true,false]
|
54
|
+
def contains_interpolation?(string)
|
55
|
+
return false unless string
|
56
|
+
Haml::Util.contains_interpolation?(string)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns whether this tag node has inline script, e.g. is of the form
|
60
|
+
# %tag= ...
|
61
|
+
#
|
62
|
+
# @param tag_node [HamlLint::Tree::TagNode]
|
63
|
+
# @return [true,false]
|
64
|
+
def tag_has_inline_script?(tag_node)
|
65
|
+
tag_with_inline_content = tag_with_inline_text(tag_node)
|
66
|
+
return false unless inline_content = inline_node_content(tag_node)
|
67
|
+
return false unless index = tag_with_inline_content.rindex(inline_content)
|
68
|
+
|
69
|
+
index -= 1
|
70
|
+
index -= 1 while [' ', '"', "'"].include?(tag_with_inline_content[index])
|
71
|
+
|
72
|
+
tag_with_inline_content[index] == '='
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns whether the inline content for a node is a string.
|
76
|
+
#
|
77
|
+
# For example, the following node has a literal string:
|
78
|
+
#
|
79
|
+
# %tag= "A literal #{string}"
|
80
|
+
#
|
81
|
+
# whereas this one does not:
|
82
|
+
#
|
83
|
+
# %tag A literal #{string}
|
84
|
+
#
|
85
|
+
# @param node [HamlLint::Tree::Node]
|
86
|
+
# @return [true,false]
|
87
|
+
def inline_content_is_string?(node)
|
88
|
+
tag_with_inline_content = tag_with_inline_text(node)
|
89
|
+
inline_content = inline_node_content(node)
|
90
|
+
|
91
|
+
index = tag_with_inline_content.rindex(inline_content) - 1
|
92
|
+
|
93
|
+
%w[' "].include?(tag_with_inline_content[index])
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get the inline content for this node.
|
97
|
+
#
|
98
|
+
# Inline content is the content that appears inline right after the
|
99
|
+
# tag/script. For example, in the code below...
|
100
|
+
#
|
101
|
+
# %tag Some inline content
|
102
|
+
#
|
103
|
+
# ..."Some inline content" would be the inline content.
|
104
|
+
#
|
105
|
+
# @param node [HamlLint::Tree::Node]
|
106
|
+
# @return [String]
|
107
|
+
def inline_node_content(node)
|
108
|
+
inline_content = node.script
|
109
|
+
|
110
|
+
if contains_interpolation?(inline_content)
|
111
|
+
strip_surrounding_quotes(inline_content)
|
112
|
+
else
|
113
|
+
inline_content
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Gets the next node following this node, ascending up the ancestor chain
|
118
|
+
# recursively if this node has no siblings.
|
119
|
+
#
|
120
|
+
# @param node [HamlLint::Tree::Node]
|
121
|
+
# @return [HamlLint::Tree::Node,nil]
|
122
|
+
def next_node(node)
|
123
|
+
return unless node
|
124
|
+
siblings = node.parent ? node.parent.children : [node]
|
125
|
+
|
126
|
+
next_sibling = siblings[siblings.index(node) + 1] if siblings.count > 1
|
127
|
+
return next_sibling if next_sibling
|
128
|
+
|
129
|
+
next_node(node.parent)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the line of the "following node" (child of this node or sibling or
|
133
|
+
# the last line in the file).
|
134
|
+
#
|
135
|
+
# @param node [HamlLint::Tree::Node]
|
136
|
+
def following_node_line(node)
|
137
|
+
[
|
138
|
+
[node.children.first, next_node(node)].compact.map(&:line),
|
139
|
+
parser.lines.count + 1,
|
140
|
+
].flatten.min
|
141
|
+
end
|
142
|
+
|
143
|
+
# Extracts all text for a tag node and normalizes it, including additional
|
144
|
+
# lines following commas or multiline bar indicators ('|')
|
145
|
+
#
|
146
|
+
# @param tag_node [HamlLine::Tree::TagNode]
|
147
|
+
# @return [String] source code of original parse node
|
148
|
+
def tag_with_inline_text(tag_node)
|
149
|
+
# Normalize each of the lines to ignore the multiline bar (|) and
|
150
|
+
# excess whitespace
|
151
|
+
parser.lines[(tag_node.line - 1)...(following_node_line(tag_node) - 1)].map do |line|
|
152
|
+
line.strip.gsub(/\|\z/, '').rstrip
|
153
|
+
end.join(' ')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module HamlLint
|
2
|
+
class NoSuchLinter < StandardError; end
|
3
|
+
|
4
|
+
# Stores all defined linters.
|
5
|
+
module LinterRegistry
|
6
|
+
@linters = []
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :linters
|
10
|
+
|
11
|
+
def included(base)
|
12
|
+
@linters << base
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_linters_from(linter_names)
|
16
|
+
linter_names.map do |linter_name|
|
17
|
+
begin
|
18
|
+
HamlLint::Linter.const_get(linter_name)
|
19
|
+
rescue NameError
|
20
|
+
raise NoSuchLinter, "Linter #{linter_name} does not exist"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|