erb_lint 0.0.37 → 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/all.rb +26 -0
- data/lib/erb_lint/cli.rb +73 -24
- data/lib/erb_lint/corrector.rb +1 -1
- data/lib/erb_lint/linter.rb +5 -5
- data/lib/erb_lint/linter_config.rb +3 -3
- data/lib/erb_lint/linter_registry.rb +2 -2
- 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 +7 -4
- data/lib/erb_lint/reporter.rb +2 -2
- data/lib/erb_lint/reporters/compact_reporter.rb +9 -3
- data/lib/erb_lint/reporters/json_reporter.rb +72 -0
- data/lib/erb_lint/reporters/multiline_reporter.rb +1 -1
- data/lib/erb_lint/runner.rb +1 -1
- 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 +9 -6
- 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
- data/lib/erb_lint.rb +1 -24
- metadata +9 -3
@@ -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
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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,
|
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 = "#{
|
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(
|
159
|
+
inherit_from = hash&.delete("inherit_from")
|
160
160
|
resolve_inheritance(hash, inherit_from)
|
161
161
|
|
162
|
-
tempfile_from(
|
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(
|
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
|