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