erb_lint 0.0.37 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/exe/erblint +1 -1
  3. data/lib/erb_lint/all.rb +26 -0
  4. data/lib/erb_lint/cli.rb +73 -24
  5. data/lib/erb_lint/corrector.rb +1 -1
  6. data/lib/erb_lint/linter.rb +5 -5
  7. data/lib/erb_lint/linter_config.rb +3 -3
  8. data/lib/erb_lint/linter_registry.rb +2 -2
  9. data/lib/erb_lint/linters/allowed_script_type.rb +7 -7
  10. data/lib/erb_lint/linters/closing_erb_tag_indent.rb +2 -2
  11. data/lib/erb_lint/linters/deprecated_classes.rb +7 -7
  12. data/lib/erb_lint/linters/erb_safety.rb +2 -2
  13. data/lib/erb_lint/linters/extra_newline.rb +1 -1
  14. data/lib/erb_lint/linters/final_newline.rb +2 -2
  15. data/lib/erb_lint/linters/hard_coded_string.rb +36 -16
  16. data/lib/erb_lint/linters/no_javascript_tag_helper.rb +8 -8
  17. data/lib/erb_lint/linters/partial_instance_variable.rb +23 -0
  18. data/lib/erb_lint/linters/require_input_autocomplete.rb +121 -0
  19. data/lib/erb_lint/linters/require_script_nonce.rb +92 -0
  20. data/lib/erb_lint/linters/right_trim.rb +1 -1
  21. data/lib/erb_lint/linters/rubocop.rb +11 -11
  22. data/lib/erb_lint/linters/rubocop_text.rb +1 -1
  23. data/lib/erb_lint/linters/self_closing_tag.rb +5 -7
  24. data/lib/erb_lint/linters/space_around_erb_tag.rb +5 -5
  25. data/lib/erb_lint/linters/space_in_html_tag.rb +6 -6
  26. data/lib/erb_lint/linters/space_indentation.rb +1 -1
  27. data/lib/erb_lint/linters/trailing_whitespace.rb +1 -1
  28. data/lib/erb_lint/offense.rb +7 -4
  29. data/lib/erb_lint/reporter.rb +2 -2
  30. data/lib/erb_lint/reporters/compact_reporter.rb +9 -3
  31. data/lib/erb_lint/reporters/json_reporter.rb +72 -0
  32. data/lib/erb_lint/reporters/multiline_reporter.rb +1 -1
  33. data/lib/erb_lint/runner.rb +1 -1
  34. data/lib/erb_lint/runner_config.rb +8 -7
  35. data/lib/erb_lint/runner_config_resolver.rb +4 -4
  36. data/lib/erb_lint/stats.rb +9 -6
  37. data/lib/erb_lint/utils/block_map.rb +2 -2
  38. data/lib/erb_lint/utils/offset_corrector.rb +1 -1
  39. data/lib/erb_lint/utils/ruby_to_erb.rb +5 -5
  40. data/lib/erb_lint/utils/severity_levels.rb +16 -0
  41. data/lib/erb_lint/version.rb +1 -1
  42. data/lib/erb_lint.rb +1 -24
  43. metadata +9 -3
@@ -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
@@ -8,7 +8,7 @@ module ERBLint
8
8
  include LinterRegistry
9
9
 
10
10
  class ConfigSchema < LinterConfig
11
- property :enforced_style, accepts: ['-', '='], default: '-'
11
+ property :enforced_style, accepts: ["-", "="], default: "-"
12
12
  end
13
13
  self.config_schema = ConfigSchema
14
14
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'better_html'
4
- require 'tempfile'
5
- require 'erb_lint/utils/offset_corrector'
3
+ require "better_html"
4
+ require "tempfile"
5
+ require "erb_lint/utils/offset_corrector"
6
6
 
7
7
  module ERBLint
8
8
  module Linters
@@ -25,7 +25,7 @@ module ERBLint
25
25
  super
26
26
  @only_cops = @config.only
27
27
  custom_config = config_from_hash(@config.rubocop_config)
28
- @rubocop_config = ::RuboCop::ConfigLoader.merge_with_default(custom_config, '')
28
+ @rubocop_config = ::RuboCop::ConfigLoader.merge_with_default(custom_config, "")
29
29
  end
30
30
 
31
31
  def run(processed_source)
@@ -68,13 +68,13 @@ module ERBLint
68
68
 
69
69
  def inspect_content(processed_source, erb_node)
70
70
  indicator, _, code_node, = *erb_node
71
- return if indicator&.children&.first == '#'
71
+ return if indicator&.children&.first == "#"
72
72
 
73
73
  original_source = code_node.loc.source
74
- trimmed_source = original_source.sub(BLOCK_EXPR, '').sub(SUFFIX_EXPR, '')
74
+ trimmed_source = original_source.sub(BLOCK_EXPR, "").sub(SUFFIX_EXPR, "")
75
75
  alignment_column = code_node.loc.column
76
76
  offset = code_node.loc.begin_pos - alignment_column
77
- aligned_source = "#{' ' * alignment_column}#{trimmed_source}"
77
+ aligned_source = "#{" " * alignment_column}#{trimmed_source}"
78
78
 
79
79
  source = rubocop_processed_source(aligned_source, processed_source.filename)
80
80
  return unless source.valid_syntax?
@@ -156,10 +156,10 @@ module ERBLint
156
156
  end
157
157
 
158
158
  def config_from_hash(hash)
159
- inherit_from = hash&.delete('inherit_from')
159
+ inherit_from = hash&.delete("inherit_from")
160
160
  resolve_inheritance(hash, inherit_from)
161
161
 
162
- tempfile_from('.erblint-rubocop', hash.to_yaml) do |tempfile|
162
+ tempfile_from(".erblint-rubocop", hash.to_yaml) do |tempfile|
163
163
  ::RuboCop::ConfigLoader.load_file(tempfile.path)
164
164
  end
165
165
  end
@@ -174,7 +174,7 @@ module ERBLint
174
174
  end
175
175
 
176
176
  def base_configs(inherit_from)
177
- regex = URI::DEFAULT_PARSER.make_regexp(%w(http https))
177
+ regex = URI::DEFAULT_PARSER.make_regexp(["http", "https"])
178
178
  configs = Array(inherit_from).compact.map do |base_name|
179
179
  if base_name =~ /\A#{regex}\z/
180
180
  ::RuboCop::ConfigLoader.load_file(::RuboCop::RemoteConfig.new(base_name, Dir.pwd))
@@ -191,7 +191,7 @@ module ERBLint
191
191
  { rubocop_correction: correction, offset: offset, bound_range: bound_range }
192
192
  end
193
193
 
194
- super(offense_range, rubocop_offense.message.strip, context)
194
+ super(offense_range, rubocop_offense.message.strip, context, rubocop_offense.severity.name)
195
195
  end
196
196
  end
197
197
  end