haml_lint 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/bin/haml-lint +7 -0
  3. data/config/default.yml +91 -0
  4. data/lib/haml_lint/cli.rb +122 -0
  5. data/lib/haml_lint/configuration.rb +97 -0
  6. data/lib/haml_lint/configuration_loader.rb +68 -0
  7. data/lib/haml_lint/constants.rb +8 -0
  8. data/lib/haml_lint/exceptions.rb +15 -0
  9. data/lib/haml_lint/file_finder.rb +69 -0
  10. data/lib/haml_lint/haml_visitor.rb +36 -0
  11. data/lib/haml_lint/lint.rb +25 -0
  12. data/lib/haml_lint/linter/alt_text.rb +12 -0
  13. data/lib/haml_lint/linter/class_attribute_with_static_value.rb +51 -0
  14. data/lib/haml_lint/linter/classes_before_ids.rb +26 -0
  15. data/lib/haml_lint/linter/consecutive_comments.rb +20 -0
  16. data/lib/haml_lint/linter/consecutive_silent_scripts.rb +23 -0
  17. data/lib/haml_lint/linter/empty_script.rb +12 -0
  18. data/lib/haml_lint/linter/html_attributes.rb +14 -0
  19. data/lib/haml_lint/linter/implicit_div.rb +20 -0
  20. data/lib/haml_lint/linter/leading_comment_space.rb +14 -0
  21. data/lib/haml_lint/linter/line_length.rb +19 -0
  22. data/lib/haml_lint/linter/multiline_pipe.rb +43 -0
  23. data/lib/haml_lint/linter/multiline_script.rb +43 -0
  24. data/lib/haml_lint/linter/object_reference_attributes.rb +14 -0
  25. data/lib/haml_lint/linter/rubocop.rb +76 -0
  26. data/lib/haml_lint/linter/ruby_comments.rb +18 -0
  27. data/lib/haml_lint/linter/space_before_script.rb +52 -0
  28. data/lib/haml_lint/linter/space_inside_hash_attributes.rb +32 -0
  29. data/lib/haml_lint/linter/tag_name.rb +13 -0
  30. data/lib/haml_lint/linter/trailing_whitespace.rb +16 -0
  31. data/lib/haml_lint/linter/unnecessary_interpolation.rb +29 -0
  32. data/lib/haml_lint/linter/unnecessary_string_output.rb +39 -0
  33. data/lib/haml_lint/linter.rb +156 -0
  34. data/lib/haml_lint/linter_registry.rb +26 -0
  35. data/lib/haml_lint/logger.rb +107 -0
  36. data/lib/haml_lint/node_transformer.rb +28 -0
  37. data/lib/haml_lint/options.rb +89 -0
  38. data/lib/haml_lint/parser.rb +87 -0
  39. data/lib/haml_lint/rake_task.rb +107 -0
  40. data/lib/haml_lint/report.rb +16 -0
  41. data/lib/haml_lint/reporter/default_reporter.rb +39 -0
  42. data/lib/haml_lint/reporter/json_reporter.rb +44 -0
  43. data/lib/haml_lint/reporter.rb +36 -0
  44. data/lib/haml_lint/ruby_parser.rb +29 -0
  45. data/lib/haml_lint/runner.rb +76 -0
  46. data/lib/haml_lint/script_extractor.rb +181 -0
  47. data/lib/haml_lint/tree/comment_node.rb +5 -0
  48. data/lib/haml_lint/tree/doctype_node.rb +5 -0
  49. data/lib/haml_lint/tree/filter_node.rb +9 -0
  50. data/lib/haml_lint/tree/haml_comment_node.rb +18 -0
  51. data/lib/haml_lint/tree/node.rb +98 -0
  52. data/lib/haml_lint/tree/plain_node.rb +5 -0
  53. data/lib/haml_lint/tree/root_node.rb +5 -0
  54. data/lib/haml_lint/tree/script_node.rb +11 -0
  55. data/lib/haml_lint/tree/silent_script_node.rb +12 -0
  56. data/lib/haml_lint/tree/tag_node.rb +221 -0
  57. data/lib/haml_lint/utils.rb +58 -0
  58. data/lib/haml_lint/version.rb +4 -0
  59. data/lib/haml_lint.rb +36 -0
  60. metadata +175 -0
@@ -0,0 +1,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