erb_lint 0.0.35 → 0.1.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/exe/erblint +1 -1
  3. data/lib/erb_lint.rb +1 -17
  4. data/lib/erb_lint/all.rb +26 -0
  5. data/lib/erb_lint/cli.rb +101 -54
  6. data/lib/erb_lint/corrector.rb +1 -1
  7. data/lib/erb_lint/linter.rb +6 -5
  8. data/lib/erb_lint/linter_config.rb +3 -3
  9. data/lib/erb_lint/linter_registry.rb +14 -5
  10. data/lib/erb_lint/linters/allowed_script_type.rb +7 -7
  11. data/lib/erb_lint/linters/closing_erb_tag_indent.rb +2 -2
  12. data/lib/erb_lint/linters/deprecated_classes.rb +7 -7
  13. data/lib/erb_lint/linters/erb_safety.rb +2 -2
  14. data/lib/erb_lint/linters/extra_newline.rb +1 -1
  15. data/lib/erb_lint/linters/final_newline.rb +2 -2
  16. data/lib/erb_lint/linters/hard_coded_string.rb +36 -16
  17. data/lib/erb_lint/linters/no_javascript_tag_helper.rb +8 -8
  18. data/lib/erb_lint/linters/partial_instance_variable.rb +23 -0
  19. data/lib/erb_lint/linters/require_input_autocomplete.rb +121 -0
  20. data/lib/erb_lint/linters/require_script_nonce.rb +92 -0
  21. data/lib/erb_lint/linters/right_trim.rb +1 -1
  22. data/lib/erb_lint/linters/rubocop.rb +11 -11
  23. data/lib/erb_lint/linters/rubocop_text.rb +1 -1
  24. data/lib/erb_lint/linters/self_closing_tag.rb +5 -7
  25. data/lib/erb_lint/linters/space_around_erb_tag.rb +5 -5
  26. data/lib/erb_lint/linters/space_in_html_tag.rb +6 -6
  27. data/lib/erb_lint/linters/space_indentation.rb +1 -1
  28. data/lib/erb_lint/linters/trailing_whitespace.rb +1 -1
  29. data/lib/erb_lint/offense.rb +15 -4
  30. data/lib/erb_lint/reporter.rb +39 -0
  31. data/lib/erb_lint/reporters/compact_reporter.rb +66 -0
  32. data/lib/erb_lint/reporters/json_reporter.rb +72 -0
  33. data/lib/erb_lint/reporters/multiline_reporter.rb +22 -0
  34. data/lib/erb_lint/runner.rb +1 -2
  35. data/lib/erb_lint/runner_config.rb +8 -7
  36. data/lib/erb_lint/runner_config_resolver.rb +4 -4
  37. data/lib/erb_lint/stats.rb +30 -0
  38. data/lib/erb_lint/utils/block_map.rb +2 -2
  39. data/lib/erb_lint/utils/offset_corrector.rb +1 -1
  40. data/lib/erb_lint/utils/ruby_to_erb.rb +5 -5
  41. data/lib/erb_lint/utils/severity_levels.rb +16 -0
  42. data/lib/erb_lint/version.rb +1 -1
  43. metadata +17 -7
@@ -25,7 +25,7 @@ module ERBLint
25
25
  add_offense(
26
26
  code_node.loc.end.adjust(begin_pos: -end_spaces.size),
27
27
  "Remove newline before `%>` to match start of tag.",
28
- ' '
28
+ " "
29
29
  )
30
30
  elsif start_with_newline && !end_with_newline
31
31
  add_offense(
@@ -39,7 +39,7 @@ module ERBLint
39
39
  add_offense(
40
40
  code_node.loc.end.adjust(begin_pos: -current_indent.size),
41
41
  "Indent `%>` on column #{erb_node.loc.column} to match start of tag.",
42
- ' ' * erb_node.loc.column
42
+ " " * erb_node.loc.column
43
43
  )
44
44
  end
45
45
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'better_html'
4
- require 'better_html/parser'
3
+ require "better_html"
4
+ require "better_html/parser"
5
5
 
6
6
  module ERBLint
7
7
  module Linters
@@ -11,7 +11,7 @@ module ERBLint
11
11
 
12
12
  class RuleSet
13
13
  include SmartProperties
14
- property :suggestion, accepts: String, default: ''
14
+ property :suggestion, accepts: String, default: ""
15
15
  property :deprecated, accepts: LinterConfig.array_of?(String), default: -> { [] }
16
16
  end
17
17
 
@@ -57,9 +57,9 @@ module ERBLint
57
57
  def class_name_with_loc(processed_source)
58
58
  Enumerator.new do |yielder|
59
59
  tags(processed_source).each do |tag|
60
- class_value = tag.attributes['class']&.value
60
+ class_value = tag.attributes["class"]&.value
61
61
  next unless class_value
62
- class_value.split(' ').each do |class_name|
62
+ class_value.split(" ").each do |class_name|
63
63
  yielder.yield(class_name, tag.loc)
64
64
  end
65
65
  end
@@ -69,7 +69,7 @@ module ERBLint
69
69
  def text_tags_content(processed_source)
70
70
  Enumerator.new do |yielder|
71
71
  script_tags(processed_source)
72
- .select { |tag| tag.attributes['type']&.value == 'text/html' }
72
+ .select { |tag| tag.attributes["type"]&.value == "text/html" }
73
73
  .each do |tag|
74
74
  index = processed_source.ast.to_a.find_index(tag.node)
75
75
  next_node = processed_source.ast.to_a[index + 1]
@@ -80,7 +80,7 @@ module ERBLint
80
80
  end
81
81
 
82
82
  def script_tags(processed_source)
83
- tags(processed_source).select { |tag| tag.name == 'script' }
83
+ tags(processed_source).select { |tag| tag.name == "script" }
84
84
  end
85
85
 
86
86
  def tags(processed_source)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'better_html'
4
- require 'better_html/test_helper/safe_erb_tester'
3
+ require "better_html"
4
+ require "better_html/test_helper/safe_erb_tester"
5
5
 
6
6
  module ERBLint
7
7
  module Linters
@@ -22,7 +22,7 @@ module ERBLint
22
22
 
23
23
  def autocorrect(_processed_source, offense)
24
24
  lambda do |corrector|
25
- corrector.replace(offense.source_range, '')
25
+ corrector.replace(offense.source_range, "")
26
26
  end
27
27
  end
28
28
  end
@@ -28,7 +28,7 @@ module ERBLint
28
28
  if final_newline.empty?
29
29
  add_offense(
30
30
  processed_source.to_source_range(file_content.size...file_content.size),
31
- 'Missing a trailing newline at the end of the file.',
31
+ "Missing a trailing newline at the end of the file.",
32
32
  :insert
33
33
  )
34
34
  else
@@ -36,7 +36,7 @@ module ERBLint
36
36
  processed_source.to_source_range(
37
37
  (file_content.size - final_newline.size + 1)...file_content.size
38
38
  ),
39
- 'Remove multiple trailing newline at the end of the file.',
39
+ "Remove multiple trailing newline at the end of the file.",
40
40
  :remove
41
41
  )
42
42
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "set"
3
- require 'better_html/tree/tag'
4
- require 'active_support/core_ext/string/inflections'
3
+ require "better_html/tree/tag"
4
+ require "active_support/core_ext/string/inflections"
5
5
 
6
6
  module ERBLint
7
7
  module Linters
@@ -13,17 +13,37 @@ module ERBLint
13
13
  MissingCorrector = Class.new(StandardError)
14
14
  MissingI18nLoadPath = Class.new(StandardError)
15
15
 
16
- ALLOWED_CORRECTORS = %w(
17
- I18nCorrector
18
- RuboCop::Corrector::I18n::HardCodedString
19
- )
20
-
21
- NON_TEXT_TAGS = Set.new(%w(script style xmp iframe noembed noframes listing))
22
- BLACK_LISTED_TEXT = Set.new(%w(       ))
16
+ ALLOWED_CORRECTORS = ["I18nCorrector", "RuboCop::Corrector::I18n::HardCodedString"]
17
+
18
+ NON_TEXT_TAGS = Set.new(["script", "style", "xmp", "iframe", "noembed", "noframes", "listing"])
19
+ TEXT_NOT_ALLOWED = Set.new([
20
+ " ",
21
+ "&",
22
+ "<",
23
+ ">",
24
+ """,
25
+ "©",
26
+ "®",
27
+ "™",
28
+ "…",
29
+ "—",
30
+ "•",
31
+ "“",
32
+ "”",
33
+ "‘",
34
+ "’",
35
+ "←",
36
+ "→",
37
+ "↓",
38
+ "↑",
39
+ " ",
40
+ " ",
41
+ " ",
42
+ ])
23
43
 
24
44
  class ConfigSchema < LinterConfig
25
45
  property :corrector, accepts: Hash, required: false, default: -> { {} }
26
- property :i18n_load_path, accepts: String, required: false, default: ''
46
+ property :i18n_load_path, accepts: String, required: false, default: ""
27
47
  end
28
48
  self.config_schema = ConfigSchema
29
49
 
@@ -65,7 +85,7 @@ module ERBLint
65
85
  return unless string.strip.length > 1
66
86
  node = ::RuboCop::AST::StrNode.new(:str, [string])
67
87
  corrector = klass.new(node, processed_source.filename, corrector_i18n_load_path, offense.source_range)
68
- corrector.autocorrect(tag_start: '<%= ', tag_end: ' %>')
88
+ corrector.autocorrect(tag_start: "<%= ", tag_end: " %>")
69
89
  rescue MissingCorrector, MissingI18nLoadPath
70
90
  nil
71
91
  end
@@ -73,20 +93,20 @@ module ERBLint
73
93
  private
74
94
 
75
95
  def check_string?(str)
76
- string = str.gsub(/\s*/, '')
77
- string.length > 1 && !BLACK_LISTED_TEXT.include?(string)
96
+ string = str.gsub(/\s*/, "")
97
+ string.length > 1 && !TEXT_NOT_ALLOWED.include?(string)
78
98
  end
79
99
 
80
100
  def load_corrector
81
- corrector_name = @config['corrector'].fetch('name') { raise MissingCorrector }
101
+ corrector_name = @config["corrector"].fetch("name") { raise MissingCorrector }
82
102
  raise ForbiddenCorrector unless ALLOWED_CORRECTORS.include?(corrector_name)
83
- require @config['corrector'].fetch('path') { raise MissingCorrector }
103
+ require @config["corrector"].fetch("path") { raise MissingCorrector }
84
104
 
85
105
  corrector_name.safe_constantize
86
106
  end
87
107
 
88
108
  def corrector_i18n_load_path
89
- @config['corrector'].fetch('i18n_load_path') { raise MissingI18nLoadPath }
109
+ @config["corrector"].fetch("i18n_load_path") { raise MissingI18nLoadPath }
90
110
  end
91
111
 
92
112
  def non_text_tag?(processed_source, text_node)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'better_html'
4
- require 'better_html/ast/node'
5
- require 'better_html/test_helper/ruby_node'
6
- require 'erb_lint/utils/block_map'
7
- require 'erb_lint/utils/ruby_to_erb'
3
+ require "better_html"
4
+ require "better_html/ast/node"
5
+ require "better_html/test_helper/ruby_node"
6
+ require "erb_lint/utils/block_map"
7
+ require "erb_lint/utils/ruby_to_erb"
8
8
 
9
9
  module ERBLint
10
10
  module Linters
@@ -21,7 +21,7 @@ module ERBLint
21
21
  parser.ast.descendants(:erb).each do |erb_node|
22
22
  indicator_node, _, code_node, _ = *erb_node
23
23
  indicator = indicator_node&.loc&.source
24
- next if indicator == '#'
24
+ next if indicator == "#"
25
25
  source = code_node.loc.source
26
26
 
27
27
  ruby_node =
@@ -63,10 +63,10 @@ module ERBLint
63
63
  return unless (0..2).cover?(argument_nodes.size)
64
64
 
65
65
  script_content = unless argument_nodes.first&.type?(:hash)
66
- Utils::RubyToERB.ruby_to_erb(argument_nodes.first, '==')
66
+ Utils::RubyToERB.ruby_to_erb(argument_nodes.first, "==")
67
67
  end
68
68
  arguments = if argument_nodes.last&.type?(:hash)
69
- ' ' + Utils::RubyToERB.html_options_to_tag_attributes(argument_nodes.last)
69
+ " " + Utils::RubyToERB.html_options_to_tag_attributes(argument_nodes.last)
70
70
  end
71
71
 
72
72
  return if end_node && script_content
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ # Checks for instance variables in partials.
6
+ class PartialInstanceVariable < Linter
7
+ include LinterRegistry
8
+
9
+ def run(processed_source)
10
+ instance_variable_regex = /\s@\w+/
11
+ return unless processed_source.filename.match?(/.*_.*.erb\z/) &&
12
+ processed_source.file_content.match?(instance_variable_regex)
13
+
14
+ add_offense(
15
+ processed_source.to_source_range(
16
+ processed_source.file_content =~ instance_variable_regex..processed_source.file_content.size
17
+ ),
18
+ "Instance variable detected in partial."
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "better_html"
4
+ require "better_html/tree/tag"
5
+
6
+ module ERBLint
7
+ module Linters
8
+ class RequireInputAutocomplete < Linter
9
+ include LinterRegistry
10
+
11
+ HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE = [
12
+ "color",
13
+ "date",
14
+ "datetime-local",
15
+ "email",
16
+ "month",
17
+ "number",
18
+ "password",
19
+ "range",
20
+ "search",
21
+ "tel",
22
+ "text",
23
+ "time",
24
+ "url",
25
+ "week",
26
+ ].freeze
27
+
28
+ FORM_HELPERS_REQUIRING_AUTOCOMPLETE = [
29
+ :date_field_tag,
30
+ :color_field_tag,
31
+ :email_field_tag,
32
+ :text_field_tag,
33
+ :utf8_enforcer_tag,
34
+ :month_field_tag,
35
+ :number_field_tag,
36
+ :password_field_tag,
37
+ :search_field_tag,
38
+ :telephone_field_tag,
39
+ :time_field_tag,
40
+ :url_field_tag,
41
+ :week_field_tag,
42
+ ].freeze
43
+
44
+ def run(processed_source)
45
+ parser = processed_source.parser
46
+
47
+ find_html_input_tags(parser)
48
+ find_rails_helper_input_tags(parser)
49
+ end
50
+
51
+ private
52
+
53
+ def find_html_input_tags(parser)
54
+ parser.nodes_with_type(:tag).each do |tag_node|
55
+ tag = BetterHtml::Tree::Tag.from_node(tag_node)
56
+
57
+ autocomplete_attribute = tag.attributes["autocomplete"]
58
+ type_attribute = tag.attributes["type"]
59
+
60
+ next if !html_input_tag?(tag) || autocomplete_present?(autocomplete_attribute)
61
+ next unless html_type_requires_autocomplete_attribute?(type_attribute)
62
+
63
+ add_offense(
64
+ tag_node.to_a[1].loc,
65
+ "Input tag is missing an autocomplete attribute. If no "\
66
+ "autocomplete behaviour is desired, use the value `off` or `nope`.",
67
+ [autocomplete_attribute]
68
+ )
69
+ end
70
+ end
71
+
72
+ def autocomplete_present?(autocomplete_attribute)
73
+ autocomplete_attribute.present? && autocomplete_attribute.value_node.present?
74
+ end
75
+
76
+ def html_input_tag?(tag)
77
+ !tag.closing? && tag.name == "input"
78
+ end
79
+
80
+ def html_type_requires_autocomplete_attribute?(type_attribute)
81
+ type_present = type_attribute.present? && type_attribute.value_node.present?
82
+ type_present && HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE.include?(type_attribute.value)
83
+ end
84
+
85
+ def find_rails_helper_input_tags(parser)
86
+ parser.ast.descendants(:erb).each do |erb_node|
87
+ indicator_node, _, code_node, _ = *erb_node
88
+ source = code_node.loc.source
89
+ ruby_node = extract_ruby_node(source)
90
+ send_node = ruby_node&.descendants(:send)&.first
91
+
92
+ next if code_comment?(indicator_node) ||
93
+ !ruby_node ||
94
+ !input_helper?(send_node) ||
95
+ source.include?("autocomplete")
96
+
97
+ add_offense(
98
+ erb_node.loc,
99
+ "Input field helper is missing an autocomplete attribute. If no "\
100
+ "autocomplete behaviour is desired, use the value `off` or `nope`.",
101
+ [erb_node, send_node]
102
+ )
103
+ end
104
+ end
105
+
106
+ def input_helper?(send_node)
107
+ FORM_HELPERS_REQUIRING_AUTOCOMPLETE.include?(send_node&.method_name)
108
+ end
109
+
110
+ def code_comment?(indicator_node)
111
+ indicator_node&.loc&.source == "#"
112
+ end
113
+
114
+ def extract_ruby_node(source)
115
+ BetterHtml::TestHelper::RubyNode.parse(source)
116
+ rescue ::Parser::SyntaxError
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "better_html"
4
+ require "better_html/tree/tag"
5
+
6
+ module ERBLint
7
+ module Linters
8
+ # Allow inline script tags in ERB that have a nonce attribute.
9
+ # This only validates inline <script> tags, as well as rails helpers like javascript_tag.
10
+ class RequireScriptNonce < Linter
11
+ include LinterRegistry
12
+
13
+ def run(processed_source)
14
+ parser = processed_source.parser
15
+
16
+ find_html_script_tags(parser)
17
+ find_rails_helper_script_tags(parser)
18
+ end
19
+
20
+ private
21
+
22
+ def find_html_script_tags(parser)
23
+ parser.nodes_with_type(:tag).each do |tag_node|
24
+ tag = BetterHtml::Tree::Tag.from_node(tag_node)
25
+ nonce_attribute = tag.attributes["nonce"]
26
+
27
+ next if !html_javascript_tag?(tag) || nonce_present?(nonce_attribute)
28
+
29
+ add_offense(
30
+ tag_node.to_a[1].loc,
31
+ "Missing a nonce attribute. Use request.content_security_policy_nonce",
32
+ [nonce_attribute]
33
+ )
34
+ end
35
+ end
36
+
37
+ def nonce_present?(nonce_attribute)
38
+ nonce_attribute.present? && nonce_attribute.value_node.present?
39
+ end
40
+
41
+ def html_javascript_tag?(tag)
42
+ !tag.closing? &&
43
+ (tag.name == "script" && !html_javascript_type_attribute?(tag))
44
+ end
45
+
46
+ def html_javascript_type_attribute?(tag)
47
+ type_attribute = tag.attributes["type"]
48
+
49
+ type_attribute &&
50
+ type_attribute.value_node.present? &&
51
+ type_attribute.value_node.to_a[1] != "text/javascript" &&
52
+ type_attribute.value_node.to_a[1] != "application/javascript"
53
+ end
54
+
55
+ def find_rails_helper_script_tags(parser)
56
+ parser.ast.descendants(:erb).each do |erb_node|
57
+ indicator_node, _, code_node, _ = *erb_node
58
+ source = code_node.loc.source
59
+ ruby_node = extract_ruby_node(source)
60
+ send_node = ruby_node&.descendants(:send)&.first
61
+
62
+ next if code_comment?(indicator_node) ||
63
+ !ruby_node ||
64
+ !tag_helper?(send_node) ||
65
+ source.include?("nonce")
66
+
67
+ add_offense(
68
+ erb_node.loc,
69
+ "Missing a nonce attribute. Use nonce: true",
70
+ [erb_node, send_node]
71
+ )
72
+ end
73
+ end
74
+
75
+ def tag_helper?(send_node)
76
+ send_node&.method_name?(:javascript_tag) ||
77
+ send_node&.method_name?(:javascript_include_tag) ||
78
+ send_node&.method_name?(:javascript_pack_tag)
79
+ end
80
+
81
+ def code_comment?(indicator_node)
82
+ indicator_node&.loc&.source == "#"
83
+ end
84
+
85
+ def extract_ruby_node(source)
86
+ BetterHtml::TestHelper::RubyNode.parse(source)
87
+ rescue ::Parser::SyntaxError
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end