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.
- checksums.yaml +4 -4
- data/exe/erblint +1 -1
- data/lib/erb_lint.rb +1 -17
- data/lib/erb_lint/all.rb +26 -0
- data/lib/erb_lint/cli.rb +101 -54
- data/lib/erb_lint/corrector.rb +1 -1
- data/lib/erb_lint/linter.rb +6 -5
- data/lib/erb_lint/linter_config.rb +3 -3
- data/lib/erb_lint/linter_registry.rb +14 -5
- data/lib/erb_lint/linters/allowed_script_type.rb +7 -7
- data/lib/erb_lint/linters/closing_erb_tag_indent.rb +2 -2
- data/lib/erb_lint/linters/deprecated_classes.rb +7 -7
- data/lib/erb_lint/linters/erb_safety.rb +2 -2
- data/lib/erb_lint/linters/extra_newline.rb +1 -1
- data/lib/erb_lint/linters/final_newline.rb +2 -2
- data/lib/erb_lint/linters/hard_coded_string.rb +36 -16
- data/lib/erb_lint/linters/no_javascript_tag_helper.rb +8 -8
- data/lib/erb_lint/linters/partial_instance_variable.rb +23 -0
- data/lib/erb_lint/linters/require_input_autocomplete.rb +121 -0
- data/lib/erb_lint/linters/require_script_nonce.rb +92 -0
- data/lib/erb_lint/linters/right_trim.rb +1 -1
- data/lib/erb_lint/linters/rubocop.rb +11 -11
- data/lib/erb_lint/linters/rubocop_text.rb +1 -1
- data/lib/erb_lint/linters/self_closing_tag.rb +5 -7
- data/lib/erb_lint/linters/space_around_erb_tag.rb +5 -5
- data/lib/erb_lint/linters/space_in_html_tag.rb +6 -6
- data/lib/erb_lint/linters/space_indentation.rb +1 -1
- data/lib/erb_lint/linters/trailing_whitespace.rb +1 -1
- data/lib/erb_lint/offense.rb +15 -4
- data/lib/erb_lint/reporter.rb +39 -0
- data/lib/erb_lint/reporters/compact_reporter.rb +66 -0
- data/lib/erb_lint/reporters/json_reporter.rb +72 -0
- data/lib/erb_lint/reporters/multiline_reporter.rb +22 -0
- data/lib/erb_lint/runner.rb +1 -2
- data/lib/erb_lint/runner_config.rb +8 -7
- data/lib/erb_lint/runner_config_resolver.rb +4 -4
- data/lib/erb_lint/stats.rb +30 -0
- data/lib/erb_lint/utils/block_map.rb +2 -2
- data/lib/erb_lint/utils/offset_corrector.rb +1 -1
- data/lib/erb_lint/utils/ruby_to_erb.rb +5 -5
- data/lib/erb_lint/utils/severity_levels.rb +16 -0
- data/lib/erb_lint/version.rb +1 -1
- 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
|
-
|
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
|
4
|
-
require
|
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[
|
60
|
+
class_value = tag.attributes["class"]&.value
|
61
61
|
next unless class_value
|
62
|
-
class_value.split(
|
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[
|
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 ==
|
83
|
+
tags(processed_source).select { |tag| tag.name == "script" }
|
84
84
|
end
|
85
85
|
|
86
86
|
def tags(processed_source)
|
@@ -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
|
-
|
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
|
-
|
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
|
4
|
-
require
|
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 =
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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:
|
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 && !
|
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[
|
101
|
+
corrector_name = @config["corrector"].fetch("name") { raise MissingCorrector }
|
82
102
|
raise ForbiddenCorrector unless ALLOWED_CORRECTORS.include?(corrector_name)
|
83
|
-
require @config[
|
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[
|
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
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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
|
-
|
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
|