haml_lint 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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,12 @@
1
+ module HamlLint
2
+ # Checks for empty scripts.
3
+ class Linter::EmptyScript < Linter
4
+ include LinterRegistry
5
+
6
+ def visit_silent_script(node)
7
+ return unless node.script =~ /\A\s*\Z/
8
+
9
+ add_lint(node, 'Empty script should be removed')
10
+ end
11
+ end
12
+ end
@@ -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