haml_lint 0.40.0 → 0.51.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/bin/haml-lint +1 -1
  3. data/config/default.yml +9 -27
  4. data/config/forced_rubocop_config.yml +180 -0
  5. data/lib/haml_lint/adapter/haml_4.rb +20 -0
  6. data/lib/haml_lint/adapter/haml_5.rb +11 -0
  7. data/lib/haml_lint/adapter/haml_6.rb +59 -0
  8. data/lib/haml_lint/adapter.rb +2 -0
  9. data/lib/haml_lint/cli.rb +8 -3
  10. data/lib/haml_lint/configuration_loader.rb +49 -13
  11. data/lib/haml_lint/document.rb +89 -8
  12. data/lib/haml_lint/exceptions.rb +6 -0
  13. data/lib/haml_lint/extensions/haml_util_unescape_interpolation_tracking.rb +35 -0
  14. data/lib/haml_lint/file_finder.rb +2 -2
  15. data/lib/haml_lint/lint.rb +10 -1
  16. data/lib/haml_lint/linter/final_newline.rb +4 -3
  17. data/lib/haml_lint/linter/implicit_div.rb +1 -1
  18. data/lib/haml_lint/linter/indentation.rb +3 -3
  19. data/lib/haml_lint/linter/no_placeholders.rb +18 -0
  20. data/lib/haml_lint/linter/repeated_id.rb +2 -1
  21. data/lib/haml_lint/linter/rubocop.rb +353 -59
  22. data/lib/haml_lint/linter/space_before_script.rb +8 -10
  23. data/lib/haml_lint/linter/trailing_empty_lines.rb +22 -0
  24. data/lib/haml_lint/linter/unnecessary_string_output.rb +1 -1
  25. data/lib/haml_lint/linter/view_length.rb +1 -1
  26. data/lib/haml_lint/linter.rb +60 -10
  27. data/lib/haml_lint/linter_registry.rb +3 -5
  28. data/lib/haml_lint/logger.rb +2 -2
  29. data/lib/haml_lint/options.rb +26 -2
  30. data/lib/haml_lint/rake_task.rb +2 -2
  31. data/lib/haml_lint/reporter/hash_reporter.rb +1 -3
  32. data/lib/haml_lint/reporter/offense_count_reporter.rb +1 -1
  33. data/lib/haml_lint/reporter/utils.rb +33 -4
  34. data/lib/haml_lint/ruby_extraction/ad_hoc_chunk.rb +24 -0
  35. data/lib/haml_lint/ruby_extraction/base_chunk.rb +114 -0
  36. data/lib/haml_lint/ruby_extraction/chunk_extractor.rb +672 -0
  37. data/lib/haml_lint/ruby_extraction/coordinator.rb +181 -0
  38. data/lib/haml_lint/ruby_extraction/haml_comment_chunk.rb +34 -0
  39. data/lib/haml_lint/ruby_extraction/implicit_end_chunk.rb +17 -0
  40. data/lib/haml_lint/ruby_extraction/interpolation_chunk.rb +26 -0
  41. data/lib/haml_lint/ruby_extraction/non_ruby_filter_chunk.rb +32 -0
  42. data/lib/haml_lint/ruby_extraction/placeholder_marker_chunk.rb +40 -0
  43. data/lib/haml_lint/ruby_extraction/ruby_filter_chunk.rb +33 -0
  44. data/lib/haml_lint/ruby_extraction/ruby_source.rb +5 -0
  45. data/lib/haml_lint/ruby_extraction/script_chunk.rb +251 -0
  46. data/lib/haml_lint/ruby_extraction/tag_attributes_chunk.rb +63 -0
  47. data/lib/haml_lint/ruby_extraction/tag_script_chunk.rb +30 -0
  48. data/lib/haml_lint/ruby_parser.rb +11 -1
  49. data/lib/haml_lint/runner.rb +35 -3
  50. data/lib/haml_lint/spec/matchers/report_lint.rb +22 -7
  51. data/lib/haml_lint/spec/normalize_indent.rb +2 -2
  52. data/lib/haml_lint/spec/shared_linter_context.rb +9 -1
  53. data/lib/haml_lint/spec/shared_rubocop_autocorrect_context.rb +158 -0
  54. data/lib/haml_lint/spec.rb +1 -0
  55. data/lib/haml_lint/tree/filter_node.rb +10 -0
  56. data/lib/haml_lint/tree/node.rb +13 -4
  57. data/lib/haml_lint/tree/script_node.rb +7 -1
  58. data/lib/haml_lint/tree/silent_script_node.rb +16 -1
  59. data/lib/haml_lint/tree/tag_node.rb +5 -9
  60. data/lib/haml_lint/utils.rb +135 -5
  61. data/lib/haml_lint/version.rb +1 -1
  62. data/lib/haml_lint/version_comparer.rb +25 -0
  63. data/lib/haml_lint.rb +12 -0
  64. metadata +29 -15
  65. data/lib/haml_lint/ruby_extractor.rb +0 -222
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ef9ca7cc6bbd3b5f9663a2bda75669062b25efeb53af9e32cf152fd7f5aee6c
4
- data.tar.gz: 35e34d39d55e13910ec90f9ee7ece9a4baa2d6bc44f2f6962b230f6387519174
3
+ metadata.gz: 71dc6acf56f28f4f6878ff300a653d1fb6c5cc6fa9da23c71813463ea9e1b67a
4
+ data.tar.gz: af2fa134ec9718c24d019e20c359c9a965d6d09490e6f57fe202654f5f7f95a3
5
5
  SHA512:
6
- metadata.gz: 376df9f9c0de3c0949bd2187bb1ba9f3bc5e381644a93c827ca085125ac2922f59f98d048d8bf548b379ebe5def5180f9b182628030f4e66454f3edf71478f9f
7
- data.tar.gz: fa1a9ad202224e1059dd7802b1aea08db8b9d11570feca891a31650dc3b10f0b991b2585ad27faa4425bc2165a2b23e799075c944250a43568e1d17c7f83f8b1
6
+ metadata.gz: 73c8a4c7fbd68cfbeacd29e5d313dcf9ae120a624a0359fecf5ca25396a8e68a0cfbad68b0ac9854af82a9233ffb26a9adf7db156f8dd3b5eb8b7a83773db035
7
+ data.tar.gz: fcb59570ff3b999f81f12208993ab75118013b0262586478145d7d0eaa9bab78002d9808757e10025324082f895bb0962743133898dd31dbb6c3db73d4c7f68c
data/bin/haml-lint CHANGED
@@ -4,5 +4,5 @@
4
4
  require 'haml_lint'
5
5
  require 'haml_lint/cli'
6
6
 
7
- logger = HamlLint::Logger.new(STDOUT)
7
+ logger = HamlLint::Logger.new($stdout)
8
8
  exit HamlLint::CLI.new(logger).run(ARGV)
data/config/default.yml CHANGED
@@ -76,6 +76,9 @@ linters:
76
76
  MultilineScript:
77
77
  enabled: true
78
78
 
79
+ NoPlaceholders:
80
+ enabled: false
81
+
79
82
  ObjectReferenceAttributes:
80
83
  enabled: true
81
84
 
@@ -85,33 +88,9 @@ linters:
85
88
 
86
89
  RuboCop:
87
90
  enabled: true
88
- # These cops are incredibly noisy when it comes to HAML templates, so we
89
- # ignore them.
90
- ignored_cops:
91
- - Lint/BlockAlignment
92
- - Lint/EndAlignment
93
- - Lint/Void
94
- - Layout/AlignHash # renamed to Layout/HashAlignment in rubocop 0.77
95
- - Layout/AlignParameters # renamed to Layout/ParameterAlignment in rubocop 0.77
96
- - Layout/ArgumentAlignment
97
- - Layout/CaseIndentation
98
- - Layout/ElseAlignment
99
- - Layout/EndOfLine
100
- - Layout/HashAlignment
101
- - Layout/IndentationWidth
102
- - Layout/LineLength # renamed from Metrics/LineLength in rubocop 0.79.0
103
- - Layout/ParameterAlignment
104
- - Layout/TrailingBlankLines # renamed to Layout/TrailingEmptyLines in rubocop 0.77
105
- - Layout/TrailingEmptyLines
106
- - Layout/TrailingWhitespace
107
- - Metrics/BlockLength
108
- - Metrics/BlockNesting
109
- - Metrics/LineLength
110
- - Naming/FileName
111
- - Style/FrozenStringLiteralComment
112
- - Style/IfUnlessModifier
113
- - Style/Next
114
- - Style/WhileUntilModifier
91
+ # Users can ignore cops using this configuration instead of editing their rubocop configuration.
92
+ # Mostly there for backward compatibility.
93
+ ignored_cops: []
115
94
 
116
95
  RubyComments:
117
96
  enabled: true
@@ -126,6 +105,9 @@ linters:
126
105
  TagName:
127
106
  enabled: true
128
107
 
108
+ TrailingEmptyLines:
109
+ enabled: true
110
+
129
111
  TrailingWhitespace:
130
112
  enabled: true
131
113
 
@@ -0,0 +1,180 @@
1
+ # These are some configurations that are required for RuboCop because:
2
+ # * HAML-Lint compiles ruby code with a particular format. If the rules mis-match that format,
3
+ # HAML-Lint would generate lints that the user cannot fix.
4
+ # * HAML-Lint can autocorrect code only if the result matches some specific format
5
+ #
6
+ # So these configuration should not be overwritable by users.
7
+
8
+ Layout/ArgumentAlignment:
9
+ # The alternative, with_fixed_indentation, breaks because we sometimes remove indentation when
10
+ # dealing with multi-line scripts. (Because a line starting with "=" adds a "HL.out = " to the
11
+ # intermediary Ruby source, which requires indentation, and the removal of the indentation)
12
+ EnforcedStyle: with_first_argument
13
+
14
+ Layout/ArrayAlignment:
15
+ # The alternative, with_fixed_indentation, breaks because we sometimes remove indentation when
16
+ # dealing with multi-line scripts. (Because a line starting with "=" adds a "HL.out = " to the
17
+ # intermediary Ruby source, which requires indentation, and the removal of the indentation)
18
+ EnforcedStyle: with_first_element
19
+
20
+ # In Haml, there are edge cases when `when` is indented, such as this one:
21
+ # - case 1
22
+ # - when 1
23
+ # - when 2
24
+ # foo
25
+ # This generates 2 `end` when building Ruby.
26
+ # So it's safer to make the `when` be not indented, to avoid auto-correct chains that could turn
27
+ # a valid `if` into an invalid `case` such as above.
28
+ Layout/CaseIndentation:
29
+ Enabled: true
30
+ EnforcedStyle: end
31
+ IndentOneStep: false # Need to force the `false`
32
+
33
+ # Need this cop so that code gets formatted similarly to Haml's indentation,
34
+ # since HAML-Lint relies on Ruby's indentation being the same as Haml's.
35
+ Layout/ElseAlignment:
36
+ Enabled: true
37
+
38
+ # Need this cop so that code gets formatted similarly to Haml's indentation,
39
+ # since HAML-Lint relies on Ruby's indentation being the same as Haml's.
40
+ Layout/EndAlignment:
41
+ EnforcedStyleAlignWith: start_of_line
42
+ Enabled: true
43
+
44
+ # We generate the ruby content, this is basically useless and should be a lint in HAML-Lint
45
+ Layout/EndOfLine:
46
+ Enabled: false
47
+
48
+ # Turning this cop on can turn
49
+ # = content_tag(:span) do
50
+ # - foo
51
+ # - bar
52
+ #
53
+ # Into
54
+ # - HL.out =
55
+ # - content_tag(:span) do
56
+ # - foo
57
+ # - bar
58
+ #
59
+ # Which is wrong... It would take too much analysis to detect and fix that situation.
60
+ Layout/MultilineAssignmentLayout:
61
+ Enabled: false
62
+
63
+ Layout/ParameterAlignment:
64
+ # The alternative, with_fixed_indentation, breaks because we sometimes remove indentation when
65
+ # dealing with multi-line scripts. (Because a line starting with "=" adds a "HL.out = " to the
66
+ # intermediary Ruby source, which requires indentation, and the removal of the indentation)
67
+ EnforcedStyle: with_first_parameter
68
+
69
+ # HamlLint generate lots of extra code which would make blocks much longer
70
+ Metrics/BlockLength:
71
+ Enabled: false
72
+
73
+ # The nesting may be due to the html's nesting nature... These lints are probably not helpful
74
+ Metrics/BlockNesting:
75
+ Enabled: false
76
+
77
+ # The file names are generated by HamlLint, so any related lint would be unfixable by the user
78
+ Naming/FileName:
79
+ Enabled: false
80
+
81
+ # HAML doesn't properly support multiline blocks using { }, only using do/end.
82
+ # If you don't consider the { } block for indentation, things "works", but the indentation is misleading.
83
+ # For example, this works:
84
+ # - a = lambda {
85
+ # - if abc
86
+ # - something
87
+ # - }
88
+ # But if you indented the 2 lines within { }, then HAML would add an extra `end` and the generated
89
+ # ruby would be invalid.
90
+ Style/BlockDelimiters:
91
+ # So we need this cop to cleanup those cases and turn them to `end`.
92
+ Enabled: true
93
+ EnforcedStyle: line_count_based
94
+ # We don't allow the default "Can be anything" exception for lambda/proc
95
+ <%= rubocop_version < '1.33' ? 'IgnoredMethods' : 'AllowedMethods' %>: []
96
+
97
+ # We don't support correcting HAML comments
98
+ Style/CommentAnnotation:
99
+ AutoCorrect: false
100
+
101
+ # If this was enabled, the equal sign would bubble up in a if like this:
102
+ # - if a
103
+ # = abc
104
+ # - else
105
+ # = bcd
106
+ # Into:
107
+ # = if a
108
+ # - abc
109
+ # - else
110
+ # - bcd
111
+ # Feels like this might be annoying or less visibly intuitive.
112
+ Style/ConditionalAssignment:
113
+ Enabled: false
114
+
115
+ # If this gets added, it wont do anything anyways.
116
+ Style/FrozenStringLiteralComment:
117
+ Enabled: false
118
+
119
+ # Looking at the changelog, this cop has quite a few bugfixes over time.
120
+ # It still has problematic behaviors for us, such as breaking a line into multiple
121
+ # ones with high indentation, which doesn't work for haml
122
+ Style/IfUnlessModifier:
123
+ AutoCorrect: false
124
+
125
+ <% if rubocop_version >= '1.37.0' %>
126
+ # This new cop can trigger on the here-doc we use for filters that contain interpolation.
127
+ # Ex:
128
+ # :javascript
129
+ # hello #{world} \. bad escape
130
+ Style/RedundantStringEscape:
131
+ Enabled: false
132
+ <% end %>
133
+
134
+ # In some case, this cop can cause a change in the spacing.
135
+ # In HAML 5.2, going from (absurd example for clarity):
136
+ # = 'abc' rescue nil
137
+ # = 'def'
138
+ # to:
139
+ # = begin
140
+ # - 'abc'
141
+ # - rescue StandardError
142
+ # - nil
143
+ # = 'def'
144
+ # Will remove the only whitespace (a \n) that is between the abc and the def.
145
+ # This could affect spacing, ex: seeing "abc def" become "abcdef" when rendering
146
+ # after running this auto-correct
147
+ Style/RescueModifier:
148
+ AutoCorrect: false
149
+
150
+ # Cops that remove commas can be a problem when lines are split on multiple ones.
151
+ # If we have a big array on more than one line, the removal of the comma generates
152
+ # invalid HAML
153
+ Style/SymbolArray:
154
+ Enabled: false
155
+
156
+ # This can easily change the order of the markers in the document, which result in un-transferable corrections
157
+ Style/UnlessElse:
158
+ AutoCorrect: false
159
+
160
+ # If an array of strings was on multiple lines, this cop will make a %w(...) on multiple lines.
161
+ # Without the comma at the end of the first line, there the resulting HAML will be invalid, since the only
162
+ # case where a script can change line is after a comma.
163
+ Style/WordArray:
164
+ AutoCorrect: false
165
+
166
+ # Not RuboCop's job
167
+ Layout/TrailingEmptyLines:
168
+ Enabled: false
169
+
170
+ <% if rubocop_version < '1.8.1' %>
171
+ # There were a few bugs with this cop that got fixed in this version.
172
+ # Before, those bugs would generate invalid Ruby code and that would make it look like HAML-lint is
173
+ # responsible, at least from the user's point of view.
174
+ Style/StringConcatenation:
175
+ Enabled: false
176
+ <% end %>
177
+
178
+ # There is already a linter dedicated to this in haml-lint
179
+ Layout/LineLength:
180
+ Enabled: false
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module HamlLint
4
6
  class Adapter
5
7
  # Adapts the Haml::Parser from Haml 4 for use in HamlLint
@@ -16,9 +18,21 @@ module HamlLint
16
18
  # @param source [String] Haml code to parse
17
19
  # @param options [Haml::Options]
18
20
  def initialize(source, options = Haml::Options.new)
21
+ @source = source
19
22
  @parser = Haml::Parser.new(source, options)
20
23
  end
21
24
 
25
+ def precompile
26
+ # Haml uses the filters as part of precompilation... we don't care about those,
27
+ # but without this tweak, it would fail on filters that are not loaded.
28
+ real_defined = Haml::Filters.defined
29
+ Haml::Filters.instance_variable_set(:@defined, Hash.new { real_defined['plain'] })
30
+
31
+ ::Haml::Engine.new(source).precompiled
32
+ ensure
33
+ Haml::Filters.instance_variable_set(:@defined, real_defined)
34
+ end
35
+
22
36
  # @!method
23
37
  # Parses the source code into an abstract syntax tree
24
38
  #
@@ -37,6 +51,12 @@ module HamlLint
37
51
  # @api private
38
52
  # @return [Haml::Parser] the Haml 4 parser
39
53
  attr_reader :parser
54
+
55
+ # The Haml code to parse
56
+ #
57
+ # @api private
58
+ # @return [String] Haml code to parse
59
+ attr_reader :source
40
60
  end
41
61
  end
42
62
  end
@@ -30,6 +30,17 @@ module HamlLint
30
30
  parser.call(source)
31
31
  end
32
32
 
33
+ def precompile
34
+ # Haml uses the filters as part of precompilation... we don't care about those,
35
+ # but without this tweak, it would fail on filters that are not loaded.
36
+ real_defined = Haml::Filters.defined
37
+ Haml::Filters.instance_variable_set(:@defined, Hash.new { real_defined['plain'] })
38
+
39
+ ::Haml::Engine.new(source).precompiled
40
+ ensure
41
+ Haml::Filters.instance_variable_set(:@defined, real_defined)
42
+ end
43
+
33
44
  private
34
45
 
35
46
  # The Haml parser to adapt for HamlLint
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamlLint
4
+ class Adapter
5
+ # Adapts the Haml::Parser from Haml 5 for use in HamlLint
6
+ # :reek:UncommunicativeModuleName
7
+ class Haml6 < Adapter
8
+ # Parses the specified Haml code into an abstract syntax tree
9
+ #
10
+ # @example
11
+ # HamlLint::Adapter::Haml6.new('%div')
12
+ #
13
+ # @api public
14
+ # @param source [String] Haml code to parse
15
+ # @param options [private Haml::Parser::ParserOptions]
16
+ def initialize(source, options = {})
17
+ @source = source
18
+ @parser = Haml::Parser.new(options)
19
+ end
20
+
21
+ # Parses the source code into an abstract syntax tree
22
+ #
23
+ # @example
24
+ # HamlLint::Adapter::Haml6.new('%div').parse
25
+ #
26
+ # @api public
27
+ # @return [Haml::Parser::ParseNode]
28
+ # @raise [Haml::Error]
29
+ def parse
30
+ parser.call(source)
31
+ end
32
+
33
+ def precompile
34
+ # Haml uses the filters as part of precompilation... we don't care about those,
35
+ # but without this tweak, it would fail on filters that are not loaded.
36
+ real_defined = Haml::Filters.registered
37
+ Haml::Filters.instance_variable_set(:@registered, Hash.new { real_defined['plain'] })
38
+
39
+ ::Haml::Engine.new.call(source)
40
+ ensure
41
+ Haml::Filters.instance_variable_set(:@registered, real_defined)
42
+ end
43
+
44
+ private
45
+
46
+ # The Haml parser to adapt for HamlLint
47
+ #
48
+ # @api private
49
+ # @return [Haml::Parser] the Haml 4 parser
50
+ attr_reader :parser
51
+
52
+ # The Haml code to parse
53
+ #
54
+ # @api private
55
+ # @return [String] Haml code to parse
56
+ attr_reader :source
57
+ end
58
+ end
59
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'haml_lint/adapter/haml_4'
4
4
  require 'haml_lint/adapter/haml_5'
5
+ require 'haml_lint/adapter/haml_6'
5
6
  require 'haml_lint/exceptions'
6
7
 
7
8
  module HamlLint
@@ -20,6 +21,7 @@ module HamlLint
20
21
  case version
21
22
  when '~> 4.0' then HamlLint::Adapter::Haml4
22
23
  when '~> 5.0', '~> 5.1', '~> 5.2' then HamlLint::Adapter::Haml5
24
+ when '~> 6.0', '~> 6.0.a', '~> 6.1', '~> 6.2' then HamlLint::Adapter::Haml6
23
25
  else fail HamlLint::Exceptions::UnknownHamlVersion, "Cannot handle Haml version: #{version}"
24
26
  end
25
27
  end
data/lib/haml_lint/cli.rb CHANGED
@@ -7,7 +7,7 @@ require 'sysexits'
7
7
 
8
8
  module HamlLint
9
9
  # Command line application interface.
10
- class CLI # rubocop:disable Metrics/ClassLength
10
+ class CLI
11
11
  # Create a CLI that outputs to the specified logger.
12
12
  #
13
13
  # @param logger [HamlLint::Logger]
@@ -34,9 +34,14 @@ module HamlLint
34
34
  # Given the provided options, execute the appropriate command.
35
35
  #
36
36
  # @return [Integer] exit status code
37
- def act_on_options(options)
37
+ def act_on_options(options) # rubocop:disable Metrics
38
38
  configure_logger(options)
39
-
39
+ if options[:debug]
40
+ ENV['HAML_LINT_DEBUG'] = 'true'
41
+ end
42
+ if options[:internal_debug]
43
+ ENV['HAML_LINT_INTERNAL_DEBUG'] = 'true'
44
+ end
40
45
  if options[:help]
41
46
  print_help(options)
42
47
  Sysexits::EX_OK
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'yaml'
5
+ require 'erb'
5
6
 
6
7
  module HamlLint
7
8
  # Manages configuration file loading.
@@ -31,7 +32,7 @@ module HamlLint
31
32
  def default_path_to_config
32
33
  directory = File.expand_path(Dir.pwd)
33
34
  config_file = possible_config_files(directory).find(&:file?)
34
- config_file ? config_file.to_path : nil
35
+ config_file&.to_path
35
36
  end
36
37
 
37
38
  # Loads the built-in default configuration.
@@ -48,13 +49,20 @@ module HamlLint
48
49
  # @option context :exclude_files [Array<String>] files that should not
49
50
  # be loaded even if they're requested via inherits_from
50
51
  # @return [HamlLint::Configuration]
51
- def load_file(file, context = {})
52
+ def load_file(file, context = {}) # rubocop:disable Metrics
52
53
  context[:loaded_files] ||= []
54
+ context[:loaded_files].map! { |config_file| File.expand_path(config_file) }
53
55
  context[:exclude_files] ||= []
54
- config = load_from_file(file)
56
+ context[:exclude_files].map! { |config_file| File.expand_path(config_file) }
57
+ config = load_from_file(File.expand_path(file))
55
58
 
56
- [default_configuration, resolve_inheritance(config, context), config]
57
- .reduce { |acc, elem| acc.merge(elem) }
59
+ configs = if context[:loaded_files].any?
60
+ [resolve_inheritance(config, context), config]
61
+ else
62
+ [default_configuration, resolve_inheritance(config, context), config]
63
+ end
64
+
65
+ configs.reduce { |acc, elem| acc.merge(elem) }
58
66
  rescue Psych::SyntaxError, Errno::ENOENT => e
59
67
  raise HamlLint::Exceptions::ConfigurationError,
60
68
  "Unable to load configuration from '#{file}': #{e}",
@@ -78,19 +86,29 @@ module HamlLint
78
86
  #
79
87
  # @param file [String]
80
88
  # @return [HamlLint::Configuration]
81
- def load_from_file(file)
82
- hash =
83
- if yaml = YAML.load_file(file)
84
- yaml.to_hash
85
- else
86
- {}
87
- end
89
+ def load_from_file(file) # rubocop:disable Metrics
90
+ content = File.read(file)
91
+
92
+ processed_content = HamlLint::Utils.process_erb(content)
93
+ hash = (YAML.safe_load(processed_content) || {}).to_hash
88
94
 
89
95
  if hash.key?('inherit_from')
90
96
  hash['inherits_from'] ||= []
91
97
  hash['inherits_from'].concat(Array(hash.delete('inherit_from')))
92
98
  end
93
99
 
100
+ if hash.key?('inherit_gem')
101
+ hash['inherits_from'] ||= []
102
+
103
+ gems = hash.delete('inherit_gem')
104
+ (gems || {}).each_pair.reverse_each do |gem_name, config_path|
105
+ Array(config_path).reverse_each do |path|
106
+ # Put gem configuration first so local configuration overrides it.
107
+ hash['inherits_from'].unshift gem_config_path(gem_name, path)
108
+ end
109
+ end
110
+ end
111
+
94
112
  HamlLint::Configuration.new(hash, file)
95
113
  end
96
114
 
@@ -129,10 +147,28 @@ module HamlLint
129
147
  # @return [HamlLint::Configuration]
130
148
  def resolve_inheritance(config, context)
131
149
  Array(config['inherits_from'])
132
- .map { |config_file| resolve(config_file, context) }
150
+ .map { |config_file| resolve(File.expand_path(config_file), context) }
133
151
  .compact
134
152
  .reduce { |acc, elem| acc.merge(elem) } || config
135
153
  end
154
+
155
+ # Resolves the config file path relative to a gem
156
+ #
157
+ # @param gem_name [String] name of the gem
158
+ # @param relative_config_path [String] path of the file to resolve, relative to the gem root
159
+ # @return [String]
160
+ def gem_config_path(gem_name, relative_config_path)
161
+ if defined?(Bundler)
162
+ gem = Bundler.load.specs[gem_name].first
163
+ gem_path = gem.full_gem_path if gem
164
+ end
165
+
166
+ gem_path ||= Gem::Specification.find_by_name(gem_name).gem_dir
167
+
168
+ File.join(gem_path, relative_config_path)
169
+ rescue Gem::LoadError => e
170
+ raise Gem::LoadError, "Unable to find gem #{gem_name}; is the gem installed? #{e}"
171
+ end
136
172
  end
137
173
  end
138
174
  end
@@ -23,6 +23,14 @@ module HamlLint
23
23
  # @return [Array<String>] original source code as an array of lines
24
24
  attr_reader :source_lines
25
25
 
26
+ # @return [Boolean] true if the source was changed (by autocorrect)
27
+ attr_reader :source_was_changed
28
+
29
+ # @return [String] the indentation used in the file
30
+ attr_reader :indentation
31
+
32
+ attr_reader :unescape_interpolation_to_original_cache
33
+
26
34
  # Parses the specified Haml code into a {Document}.
27
35
  #
28
36
  # @param source [String] Haml code to parse
@@ -32,22 +40,80 @@ module HamlLint
32
40
  def initialize(source, options)
33
41
  @config = options[:config]
34
42
  @file = options.fetch(:file, STRING_SOURCE)
35
-
43
+ @source_was_changed = false
36
44
  process_source(source)
37
45
  end
38
46
 
47
+ # Returns the last non empty line of the document or 1 if all lines are empty
48
+ #
49
+ # @return [Integer] last non empty line of the document or 1 if all lines are empty
50
+ def last_non_empty_line
51
+ index = source_lines.rindex { |l| !l.empty? }
52
+ (index || 0) + 1
53
+ end
54
+
55
+ # Reparses the new source and remember that the document was changed
56
+ # Used when auto-correct does changes to the file. If the source hasn't changed,
57
+ # then the document will not be marked as changed.
58
+ #
59
+ # If the new_source fails to parse, automatically reparses the previous source
60
+ # to bring the document back to how it should be before re-raising the parse exception
61
+ #
62
+ # @param source [String] Haml code to parse
63
+ def change_source(new_source)
64
+ return if new_source == @source
65
+ check_new_source_compatible(new_source)
66
+
67
+ old_source = @source
68
+ begin
69
+ process_source(new_source)
70
+ @source_was_changed = true
71
+ rescue HamlLint::Exceptions::ParseError
72
+ # Reprocess the previous_source so that other linters can work on this document
73
+ # object from a clean slate
74
+ process_source(old_source)
75
+ raise
76
+ end
77
+ nil
78
+ end
79
+
80
+ def write_to_disk!
81
+ return unless @source_was_changed
82
+ if file == STRING_SOURCE
83
+ raise HamlLint::Exceptions::InvalidFilePath, 'Cannot write without :file option'
84
+ end
85
+ File.write(file, unstrip_frontmatter(source))
86
+ @source_was_changed = false
87
+ end
88
+
39
89
  private
40
90
 
41
91
  # @param source [String] Haml code to parse
42
92
  # @raise [HamlLint::Exceptions::ParseError] if there was a problem parsing
43
- def process_source(source)
93
+ def process_source(source) # rubocop:disable Metrics/MethodLength
44
94
  @source = process_encoding(source)
45
95
  @source = strip_frontmatter(source)
46
- @source_lines = @source.split(/\r\n|\r|\n/)
47
-
48
- @tree = process_tree(HamlLint::Adapter.detect_class.new(@source).parse)
96
+ # the -1 is to keep the empty strings at the end of the array when the source
97
+ # ended with multiple new-lines
98
+ @source_lines = @source.split(/\r\n|\r|\n/, -1)
99
+ adapter = HamlLint::Adapter.detect_class.new(@source)
100
+ parsed_tree = adapter.parse
101
+ @indentation = adapter.send(:parser).instance_variable_get(:@indentation)
102
+ @tree = process_tree(parsed_tree)
103
+ @unescape_interpolation_to_original_cache =
104
+ Haml::Util.unescape_interpolation_to_original_cache_take_and_wipe
49
105
  rescue Haml::Error => e
50
- error = HamlLint::Exceptions::ParseError.new(e.message, e.line)
106
+ location = if e.line
107
+ "#{@file}:#{e.line}"
108
+ else
109
+ @file
110
+ end
111
+ msg = if ENV['HAML_LINT_DEBUG'] == 'true'
112
+ "#{location} (DEBUG: source follows) - #{e.message}\n#{source}\n------"
113
+ else
114
+ "#{location} - #{e.message}"
115
+ end
116
+ error = HamlLint::Exceptions::ParseError.new(msg, e.line)
51
117
  raise error
52
118
  end
53
119
 
@@ -114,11 +180,26 @@ module HamlLint
114
180
  (---|\.\.\.)\s*$\n?/mx
115
181
 
116
182
  if config['skip_frontmatter'] && match = source.match(frontmatter)
117
- newlines = match[0].count("\n")
118
- source.sub!(frontmatter, "\n" * newlines)
183
+ @stripped_frontmatter = match[0]
184
+ @nb_newlines_for_frontmatter = match[0].count("\n")
185
+ source.sub!(frontmatter, "\n" * @nb_newlines_for_frontmatter)
119
186
  end
120
187
 
121
188
  source
122
189
  end
190
+
191
+ def check_new_source_compatible(new_source)
192
+ if @stripped_frontmatter && !new_source.start_with?("\n" * @nb_newlines_for_frontmatter)
193
+ raise HamlLint::Exceptions::IncompatibleNewSource,
194
+ "Internal error: new_source doesn't start with enough newlines for the Front Matter that was stripped"
195
+ end
196
+ end
197
+
198
+ def unstrip_frontmatter(source)
199
+ return source unless @stripped_frontmatter
200
+ check_new_source_compatible(source)
201
+
202
+ source.sub("\n" * @nb_newlines_for_frontmatter, @stripped_frontmatter)
203
+ end
123
204
  end
124
205
  end
@@ -5,6 +5,12 @@ module HamlLint::Exceptions
5
5
  # Raised when a {Configuration} could not be loaded from a file.
6
6
  class ConfigurationError < StandardError; end
7
7
 
8
+ # Raised trying to change source with incompatible one (ex: due to frontmatter)
9
+ class IncompatibleNewSource < StandardError; end
10
+
11
+ # Raised when linter's autocorrection cause an infinite loop
12
+ class InfiniteLoopError < StandardError; end
13
+
8
14
  # Raised when invalid/incompatible command line options are provided.
9
15
  class InvalidCLIOption < StandardError; end
10
16