haml_lint 0.40.0 → 0.51.0

Sign up to get free protection for your applications and to get access to all the features.
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