erb_lint 0.0.18 → 0.0.19
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/lib/erb_lint.rb +0 -1
- data/lib/erb_lint/linters/allowed_script_type.rb +79 -0
- data/lib/erb_lint/linters/deprecated_classes.rb +1 -0
- data/lib/erb_lint/linters/erb_safety.rb +26 -8
- data/lib/erb_lint/linters/no_javascript_tag_helper.rb +93 -0
- data/lib/erb_lint/linters/parser_errors.rb +19 -0
- data/lib/erb_lint/linters/right_trim.rb +37 -0
- data/lib/erb_lint/linters/rubocop.rb +27 -21
- data/lib/erb_lint/linters/rubocop_text.rb +2 -3
- data/lib/erb_lint/linters/space_around_erb_tag.rb +49 -0
- data/lib/erb_lint/offense.rb +3 -2
- data/lib/erb_lint/runner_config.rb +6 -3
- data/lib/erb_lint/utils/block_map.rb +227 -0
- data/lib/erb_lint/utils/offset_corrector.rb +56 -0
- data/lib/erb_lint/utils/ruby_to_erb.rb +51 -0
- data/lib/erb_lint/version.rb +1 -1
- metadata +12 -5
- data/lib/erb_lint/offset_corrector.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2a9562a665d609771d10b87e821c7d71a312882
|
4
|
+
data.tar.gz: 47b5284105cc9c12311ce4233b7306dc5c8b72e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1b33b2d06509bfaa8f91e8f66458b80b13530964ec5976823a2dc75c3a98e0e133601d83e5a42b462960d743b4432df93c4d599944a71a2a893cfe9948f1565
|
7
|
+
data.tar.gz: 938c959af55a5260eee35f89810c4193edebdefbdc363910acddf0c70a3a19b2307366821ba58d54e98f3be419c9c91e2b7aab845e4980db97e549bcd81c21f0
|
data/lib/erb_lint.rb
CHANGED
@@ -6,7 +6,6 @@ require 'erb_lint/linter_config'
|
|
6
6
|
require 'erb_lint/linter_registry'
|
7
7
|
require 'erb_lint/linter'
|
8
8
|
require 'erb_lint/offense'
|
9
|
-
require 'erb_lint/offset_corrector'
|
10
9
|
require 'erb_lint/processed_source'
|
11
10
|
require 'erb_lint/runner_config'
|
12
11
|
require 'erb_lint/runner'
|
@@ -0,0 +1,79 @@
|
|
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 `<script>` tags in ERB that have specific `type` attributes.
|
9
|
+
# This only validates inline `<script>` tags, a separate rubocop cop
|
10
|
+
# may be used to enforce the same rule when `javascript_tag` is called.
|
11
|
+
class AllowedScriptType < Linter
|
12
|
+
include LinterRegistry
|
13
|
+
|
14
|
+
class ConfigSchema < LinterConfig
|
15
|
+
property :allowed_types, accepts: array_of?(String),
|
16
|
+
default: ['text/javascript']
|
17
|
+
property :allow_blank, accepts: [true, false], default: true, reader: :allow_blank?
|
18
|
+
property :disallow_inline_scripts, accepts: [true, false], default: false, reader: :disallow_inline_scripts?
|
19
|
+
end
|
20
|
+
self.config_schema = ConfigSchema
|
21
|
+
|
22
|
+
def offenses(processed_source)
|
23
|
+
parser = processed_source.parser
|
24
|
+
[].tap do |offenses|
|
25
|
+
parser.nodes_with_type(:tag).each do |tag_node|
|
26
|
+
tag = BetterHtml::Tree::Tag.from_node(tag_node)
|
27
|
+
next if tag.closing?
|
28
|
+
next unless tag.name == 'script'
|
29
|
+
|
30
|
+
if @config.disallow_inline_scripts?
|
31
|
+
name_node = tag_node.to_a[1]
|
32
|
+
offenses << Offense.new(
|
33
|
+
self,
|
34
|
+
processed_source.to_source_range(name_node.loc.start, name_node.loc.stop),
|
35
|
+
"Avoid using inline `<script>` tags altogether. "\
|
36
|
+
"Instead, move javascript code into a static file."
|
37
|
+
)
|
38
|
+
next
|
39
|
+
end
|
40
|
+
|
41
|
+
type_attribute = tag.attributes['type']
|
42
|
+
type_present = type_attribute.present? && type_attribute.value_node.present?
|
43
|
+
|
44
|
+
if !type_present && !@config.allow_blank?
|
45
|
+
name_node = tag_node.to_a[1]
|
46
|
+
offenses << Offense.new(
|
47
|
+
self,
|
48
|
+
processed_source.to_source_range(name_node.loc.start, name_node.loc.stop),
|
49
|
+
"Missing a `type=\"text/javascript\"` attribute to `<script>` tag.",
|
50
|
+
[type_attribute]
|
51
|
+
)
|
52
|
+
elsif type_present && !@config.allowed_types.include?(type_attribute.value)
|
53
|
+
offenses << Offense.new(
|
54
|
+
self,
|
55
|
+
processed_source.to_source_range(type_attribute.loc.start, tag_node.loc.stop),
|
56
|
+
"Avoid using #{type_attribute.value.inspect} as type for `<script>` tag. "\
|
57
|
+
"Must be one of: #{@config.allowed_types.join(', ')}"\
|
58
|
+
"#{' (or no type attribute)' if @config.allow_blank?}."
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def autocorrect(processed_source, offense)
|
66
|
+
return unless offense.context
|
67
|
+
lambda do |corrector|
|
68
|
+
type_attribute, = *offense.context
|
69
|
+
if type_attribute.nil?
|
70
|
+
corrector.insert_after(offense.source_range, ' type="text/javascript"')
|
71
|
+
elsif !type_attribute.value.present?
|
72
|
+
range = processed_source.to_source_range(type_attribute.node.loc.start, type_attribute.node.loc.stop)
|
73
|
+
corrector.replace(range, 'type="text/javascript"')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -8,7 +8,6 @@ module ERBLint
|
|
8
8
|
# Detect unsafe ruby interpolations into javascript.
|
9
9
|
class ErbSafety < Linter
|
10
10
|
include LinterRegistry
|
11
|
-
include BetterHtml::TestHelper::SafeErbTester
|
12
11
|
|
13
12
|
class ConfigSchema < LinterConfig
|
14
13
|
property :better_html_config, accepts: String
|
@@ -23,19 +22,38 @@ module ERBLint
|
|
23
22
|
|
24
23
|
def offenses(processed_source)
|
25
24
|
offenses = []
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
|
26
|
+
parser = BetterHtml::Parser.new(processed_source.file_content, template_language: :html)
|
27
|
+
testers_for(parser).each do |tester|
|
28
|
+
tester.validate
|
29
|
+
tester.errors.each do |error|
|
30
|
+
offenses << Offense.new(
|
31
|
+
self,
|
32
|
+
processed_source.to_source_range(error.location.start, error.location.stop),
|
33
|
+
error.message
|
34
|
+
)
|
35
|
+
end
|
33
36
|
end
|
34
37
|
offenses
|
35
38
|
end
|
36
39
|
|
37
40
|
private
|
38
41
|
|
42
|
+
def tester_classes
|
43
|
+
[
|
44
|
+
BetterHtml::TestHelper::SafeErb::NoStatements,
|
45
|
+
BetterHtml::TestHelper::SafeErb::AllowedScriptType,
|
46
|
+
BetterHtml::TestHelper::SafeErb::TagInterpolation,
|
47
|
+
BetterHtml::TestHelper::SafeErb::ScriptInterpolation,
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def testers_for(parser)
|
52
|
+
tester_classes.map do |tester_klass|
|
53
|
+
tester_klass.new(parser, config: better_html_config)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
39
57
|
def better_html_config
|
40
58
|
@better_html_config ||= begin
|
41
59
|
config_hash =
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'better_html/ast/node'
|
4
|
+
require 'better_html/test_helper/ruby_node'
|
5
|
+
require 'erb_lint/utils/block_map'
|
6
|
+
require 'erb_lint/utils/ruby_to_erb'
|
7
|
+
|
8
|
+
module ERBLint
|
9
|
+
module Linters
|
10
|
+
class NoJavascriptTagHelper < Linter
|
11
|
+
include LinterRegistry
|
12
|
+
|
13
|
+
class ConfigSchema < LinterConfig
|
14
|
+
property :correction_style, converts: :to_sym, accepts: [:cdata, :plain], default: :cdata
|
15
|
+
end
|
16
|
+
self.config_schema = ConfigSchema
|
17
|
+
|
18
|
+
def offenses(processed_source)
|
19
|
+
offenses = []
|
20
|
+
|
21
|
+
parser = processed_source.parser
|
22
|
+
parser.ast.descendants(:erb).each do |erb_node|
|
23
|
+
indicator_node, _, code_node, _ = *erb_node
|
24
|
+
indicator = indicator_node&.loc&.source
|
25
|
+
next if indicator == '#'
|
26
|
+
source = code_node.loc.source
|
27
|
+
|
28
|
+
next unless (ruby_node = BetterHtml::TestHelper::RubyNode.parse(source))
|
29
|
+
send_node = ruby_node.descendants(:send).first
|
30
|
+
next unless send_node&.method_name?(:javascript_tag)
|
31
|
+
|
32
|
+
offenses << Offense.new(
|
33
|
+
self,
|
34
|
+
processed_source.to_source_range(erb_node.loc.start, erb_node.loc.stop),
|
35
|
+
"Avoid using 'javascript_tag do' as it confuses tests "\
|
36
|
+
"that validate html, use inline <script> instead",
|
37
|
+
[erb_node, send_node]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
offenses
|
42
|
+
end
|
43
|
+
|
44
|
+
def autocorrect(processed_source, offense)
|
45
|
+
lambda do |corrector|
|
46
|
+
correct_offense(processed_source, offense, corrector)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def correct_offense(processed_source, offense, corrector)
|
53
|
+
erb_node, send_node = *offense.context
|
54
|
+
block_map = Utils::BlockMap.new(processed_source)
|
55
|
+
nodes = block_map.find_connected_nodes(erb_node) || [erb_node]
|
56
|
+
return unless (1..2).cover?(nodes.size)
|
57
|
+
|
58
|
+
begin_node, end_node = nodes
|
59
|
+
begin_range = processed_source
|
60
|
+
.to_source_range(begin_node.loc.start, begin_node.loc.stop)
|
61
|
+
end_range = processed_source
|
62
|
+
.to_source_range(end_node.loc.start, end_node.loc.stop) if end_node
|
63
|
+
|
64
|
+
argument_nodes = send_node.arguments
|
65
|
+
return unless (0..2).cover?(argument_nodes.size)
|
66
|
+
|
67
|
+
script_content = unless argument_nodes.first&.type?(:hash)
|
68
|
+
Utils::RubyToERB.ruby_to_erb(argument_nodes.first, '==')
|
69
|
+
end
|
70
|
+
arguments = if argument_nodes.last&.type?(:hash)
|
71
|
+
' ' + Utils::RubyToERB.html_options_to_tag_attributes(argument_nodes.last)
|
72
|
+
end
|
73
|
+
|
74
|
+
return if end_node && script_content
|
75
|
+
|
76
|
+
if end_node
|
77
|
+
begin_content = "<script#{arguments}>"
|
78
|
+
begin_content += "\n//<![CDATA[\n" if @config.correction_style == :cdata
|
79
|
+
corrector.replace(begin_range, begin_content)
|
80
|
+
end_content = "</script>"
|
81
|
+
end_content = "\n//]]>\n" + end_content if @config.correction_style == :cdata
|
82
|
+
corrector.replace(end_range, end_content)
|
83
|
+
elsif script_content
|
84
|
+
script_content = "\n//<![CDATA[\n#{script_content}\n//]]>\n" if @config.correction_style == :cdata
|
85
|
+
corrector.replace(begin_range,
|
86
|
+
"<script#{arguments}>#{script_content}</script>")
|
87
|
+
end
|
88
|
+
rescue Utils::RubyToERB::Error, Utils::BlockMap::ParseError
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Linters
|
5
|
+
class ParserErrors < Linter
|
6
|
+
include LinterRegistry
|
7
|
+
|
8
|
+
def offenses(processed_source)
|
9
|
+
processed_source.parser.parser_errors.map do |error|
|
10
|
+
Offense.new(
|
11
|
+
self,
|
12
|
+
processed_source.to_source_range(error.loc.start, error.loc.stop - 1),
|
13
|
+
"#{error.message} (at #{error.loc.source})"
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Linters
|
5
|
+
# In ERB, right trim can be either =%> or -%>
|
6
|
+
# this linter will force one or the other.
|
7
|
+
class RightTrim < Linter
|
8
|
+
include LinterRegistry
|
9
|
+
|
10
|
+
class ConfigSchema < LinterConfig
|
11
|
+
property :enforced_style, accepts: ['-', '='], default: '-'
|
12
|
+
end
|
13
|
+
self.config_schema = ConfigSchema
|
14
|
+
|
15
|
+
def offenses(processed_source)
|
16
|
+
[].tap do |offenses|
|
17
|
+
processed_source.ast.descendants(:erb).each do |erb_node|
|
18
|
+
_, _, _, trim_node = *erb_node
|
19
|
+
next if trim_node.nil? || trim_node.loc.source == @config.enforced_style
|
20
|
+
|
21
|
+
offenses << Offense.new(
|
22
|
+
self,
|
23
|
+
processed_source.to_source_range(trim_node.loc.start, trim_node.loc.stop),
|
24
|
+
"Prefer #{@config.enforced_style}%> instead of #{trim_node.loc.source}%> for trimming on the right."
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def autocorrect(_processed_source, offense)
|
31
|
+
lambda do |corrector|
|
32
|
+
corrector.replace(offense.source_range, @config.enforced_style)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'better_html'
|
4
4
|
require 'rubocop'
|
5
5
|
require 'tempfile'
|
6
|
+
require 'erb_lint/utils/offset_corrector'
|
6
7
|
|
7
8
|
module ERBLint
|
8
9
|
module Linters
|
@@ -12,7 +13,7 @@ module ERBLint
|
|
12
13
|
|
13
14
|
class ConfigSchema < LinterConfig
|
14
15
|
property :only, accepts: array_of?(String)
|
15
|
-
property :rubocop_config, accepts: Hash
|
16
|
+
property :rubocop_config, accepts: Hash, default: {}
|
16
17
|
end
|
17
18
|
|
18
19
|
self.config_schema = ConfigSchema
|
@@ -29,17 +30,16 @@ module ERBLint
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def offenses(processed_source)
|
32
|
-
|
33
|
-
descendant_nodes(processed_source).each do |erb_node|
|
33
|
+
descendant_nodes(processed_source).each_with_object([]) do |erb_node, offenses|
|
34
34
|
offenses.push(*inspect_content(processed_source, erb_node))
|
35
35
|
end
|
36
|
-
offenses
|
37
36
|
end
|
38
37
|
|
39
38
|
def autocorrect(processed_source, offense)
|
40
|
-
return unless offense.
|
39
|
+
return unless offense.is_a?(OffenseWithCorrection)
|
40
|
+
|
41
41
|
lambda do |corrector|
|
42
|
-
passthrough = OffsetCorrector.new(
|
42
|
+
passthrough = Utils::OffsetCorrector.new(
|
43
43
|
processed_source,
|
44
44
|
corrector,
|
45
45
|
offense.offset,
|
@@ -77,12 +77,15 @@ module ERBLint
|
|
77
77
|
source = rubocop_processed_source(aligned_source)
|
78
78
|
return unless source.valid_syntax?
|
79
79
|
|
80
|
-
offenses = []
|
81
80
|
team = build_team
|
82
81
|
team.inspect_file(source)
|
83
|
-
team.cops.
|
84
|
-
|
85
|
-
|
82
|
+
team.cops.each_with_object([]) do |cop, offenses|
|
83
|
+
correction_offset = 0
|
84
|
+
cop.offenses.reject(&:disabled?).each do |rubocop_offense|
|
85
|
+
if rubocop_offense.corrected?
|
86
|
+
correction = cop.corrections[correction_offset]
|
87
|
+
correction_offset += 1
|
88
|
+
end
|
86
89
|
|
87
90
|
offset = code_node.loc.start - alignment_column
|
88
91
|
offense_range = processed_source.to_source_range(
|
@@ -95,18 +98,9 @@ module ERBLint
|
|
95
98
|
code_node.loc.stop
|
96
99
|
)
|
97
100
|
|
98
|
-
offenses <<
|
99
|
-
OffenseWithCorrection.new(
|
100
|
-
self,
|
101
|
-
offense_range,
|
102
|
-
rubocop_offense.message.strip,
|
103
|
-
correction: correction,
|
104
|
-
offset: offset,
|
105
|
-
bound_range: bound_range,
|
106
|
-
)
|
101
|
+
offenses << add_offense(rubocop_offense, offense_range, correction, offset, bound_range)
|
107
102
|
end
|
108
103
|
end
|
109
|
-
offenses
|
110
104
|
end
|
111
105
|
|
112
106
|
def tempfile_from(filename, content)
|
@@ -149,7 +143,7 @@ module ERBLint
|
|
149
143
|
end
|
150
144
|
|
151
145
|
def config_from_hash(hash)
|
152
|
-
inherit_from = hash
|
146
|
+
inherit_from = hash&.delete('inherit_from')
|
153
147
|
resolve_inheritance(hash, inherit_from)
|
154
148
|
|
155
149
|
tempfile_from('.erblint-rubocop', hash.to_yaml) do |tempfile|
|
@@ -178,6 +172,18 @@ module ERBLint
|
|
178
172
|
|
179
173
|
configs.compact
|
180
174
|
end
|
175
|
+
|
176
|
+
def add_offense(offense, offense_range, correction, offset, bound_range)
|
177
|
+
if offense.corrected?
|
178
|
+
klass = OffenseWithCorrection
|
179
|
+
options = { correction: correction, offset: offset, bound_range: bound_range }
|
180
|
+
else
|
181
|
+
klass = Offense
|
182
|
+
options = {}
|
183
|
+
end
|
184
|
+
|
185
|
+
klass.new(self, offense_range, offense.message.strip, **options)
|
186
|
+
end
|
181
187
|
end
|
182
188
|
end
|
183
189
|
end
|
@@ -27,11 +27,10 @@ module ERBLint
|
|
27
27
|
erb_nodes
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def cop_classes
|
31
31
|
selected_cops = RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
|
32
|
-
cop_classes = RuboCop::Cop::Registry.new(selected_cops)
|
33
32
|
|
34
|
-
RuboCop::Cop::
|
33
|
+
RuboCop::Cop::Registry.new(selected_cops)
|
35
34
|
end
|
36
35
|
end
|
37
36
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Linters
|
5
|
+
# Enforce a single space after `<%` and before `%>` in the erb source.
|
6
|
+
# This linter ignores opening erb tags (`<%`) that are followed by a newline,
|
7
|
+
# and closing erb tags (`%>`) that are preceeded by a newline.
|
8
|
+
class SpaceAroundErbTag < Linter
|
9
|
+
include LinterRegistry
|
10
|
+
|
11
|
+
START_SPACES = /\A([[:space:]]*)/m
|
12
|
+
END_SPACES = /([[:space:]]*)\z/m
|
13
|
+
|
14
|
+
def offenses(processed_source)
|
15
|
+
[].tap do |offenses|
|
16
|
+
processed_source.ast.descendants(:erb).each do |erb_node|
|
17
|
+
indicator, ltrim, code_node, rtrim = *erb_node
|
18
|
+
code = code_node.children.first
|
19
|
+
|
20
|
+
start_spaces = code.match(START_SPACES)&.captures&.first || ""
|
21
|
+
if start_spaces.size != 1 && !start_spaces.include?("\n")
|
22
|
+
offenses << Offense.new(
|
23
|
+
self,
|
24
|
+
processed_source.to_source_range(code_node.loc.start, code_node.loc.start + start_spaces.size - 1),
|
25
|
+
"Use 1 space after `<%#{indicator&.loc&.source}#{ltrim&.loc&.source}` "\
|
26
|
+
"instead of #{start_spaces.size} space#{'s' if start_spaces.size > 1}."
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
end_spaces = code.match(END_SPACES)&.captures&.first || ""
|
31
|
+
next unless end_spaces.size != 1 && !end_spaces.include?("\n")
|
32
|
+
offenses << Offense.new(
|
33
|
+
self,
|
34
|
+
processed_source.to_source_range(code_node.loc.stop - end_spaces.size + 1, code_node.loc.stop),
|
35
|
+
"Use 1 space before `#{rtrim&.loc&.source}%>` "\
|
36
|
+
"instead of #{end_spaces.size} space#{'s' if start_spaces.size > 1}."
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def autocorrect(_processed_source, offense)
|
43
|
+
lambda do |corrector|
|
44
|
+
corrector.replace(offense.source_range, ' ')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/erb_lint/offense.rb
CHANGED
@@ -3,15 +3,16 @@
|
|
3
3
|
module ERBLint
|
4
4
|
# Defines common functionality available to all linters.
|
5
5
|
class Offense
|
6
|
-
attr_reader :linter, :source_range, :message
|
6
|
+
attr_reader :linter, :source_range, :message, :context
|
7
7
|
|
8
|
-
def initialize(linter, source_range, message)
|
8
|
+
def initialize(linter, source_range, message, context = nil)
|
9
9
|
unless source_range.is_a?(Parser::Source::Range)
|
10
10
|
raise ArgumentError, "expected Parser::Source::Range for arg 2"
|
11
11
|
end
|
12
12
|
@linter = linter
|
13
13
|
@source_range = source_range
|
14
14
|
@message = message
|
15
|
+
@context = context
|
15
16
|
end
|
16
17
|
|
17
18
|
def inspect
|
@@ -38,9 +38,12 @@ module ERBLint
|
|
38
38
|
def default
|
39
39
|
new(
|
40
40
|
linters: {
|
41
|
-
FinalNewline: {
|
42
|
-
|
43
|
-
},
|
41
|
+
FinalNewline: { enabled: true },
|
42
|
+
ParserErrors: { enabled: true },
|
43
|
+
RightTrim: { enabled: true },
|
44
|
+
SpaceAroundErbTag: { enabled: true },
|
45
|
+
NoJavascriptTagHelper: { enabled: true },
|
46
|
+
AllowedScriptType: { enabled: true },
|
44
47
|
},
|
45
48
|
)
|
46
49
|
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'better_html/ast/node'
|
4
|
+
require 'better_html/test_helper/ruby_node'
|
5
|
+
|
6
|
+
module ERBLint
|
7
|
+
module Utils
|
8
|
+
class BlockMap
|
9
|
+
attr_reader :connections
|
10
|
+
|
11
|
+
class ParseError < StandardError; end
|
12
|
+
|
13
|
+
def initialize(processed_source)
|
14
|
+
@processed_source = processed_source
|
15
|
+
@entries = []
|
16
|
+
@connections = []
|
17
|
+
@ruby_code = ""
|
18
|
+
build_map
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_connected_nodes(other)
|
22
|
+
connection = @connections.find { |conn| conn.include?(other) }
|
23
|
+
connection&.nodes
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def erb_nodes
|
29
|
+
erb_ast.descendants(:erb).sort { |a, b| a.loc.start <=> b.loc.start }
|
30
|
+
end
|
31
|
+
|
32
|
+
class Entry
|
33
|
+
attr_reader :node, :erb_range, :ruby_range
|
34
|
+
|
35
|
+
def initialize(node, ruby_range)
|
36
|
+
@node = node
|
37
|
+
@erb_range = node.loc.range
|
38
|
+
@ruby_range = ruby_range
|
39
|
+
end
|
40
|
+
|
41
|
+
def contains_ruby_range?(range)
|
42
|
+
range.begin >= @ruby_range.begin && range.end <= @ruby_range.end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ConnectedErbNodes
|
47
|
+
attr_reader :type, :nodes
|
48
|
+
|
49
|
+
def initialize(type, nodes)
|
50
|
+
@type = type
|
51
|
+
@nodes = ordered(nodes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def concat(other)
|
55
|
+
@nodes = ordered(@nodes.concat(other.nodes))
|
56
|
+
end
|
57
|
+
|
58
|
+
def include?(other)
|
59
|
+
@nodes.map(&:loc).include?(other.loc)
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"\#<#{self.class.name} type=#{type.inspect} nodes=#{nodes.inspect}>"
|
64
|
+
end
|
65
|
+
|
66
|
+
def &(other)
|
67
|
+
nodes.select { |node| other.include?(node) }
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def ordered(nodes)
|
73
|
+
nodes
|
74
|
+
.uniq(&:loc)
|
75
|
+
.sort { |a, b| a.loc.start <=> b.loc.start }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_map
|
80
|
+
erb_nodes.each do |erb_node|
|
81
|
+
indicator_node, _, code_node, _ = *erb_node
|
82
|
+
length = code_node.loc.stop - code_node.loc.start
|
83
|
+
start = current_pos
|
84
|
+
if indicator_node.nil?
|
85
|
+
append("#{code_node.loc.source}\n")
|
86
|
+
elsif block?(code_node.loc.source)
|
87
|
+
append("src= #{code_node.loc.source}\n")
|
88
|
+
start += 5
|
89
|
+
else
|
90
|
+
append("src=(#{code_node.loc.source});\n")
|
91
|
+
start += 5
|
92
|
+
end
|
93
|
+
ruby_range = Range.new(start, start + length)
|
94
|
+
@entries << Entry.new(erb_node, ruby_range)
|
95
|
+
end
|
96
|
+
|
97
|
+
ruby_node = BetterHtml::TestHelper::RubyNode.parse(@ruby_code)
|
98
|
+
raise ParseError unless ruby_node
|
99
|
+
|
100
|
+
ruby_node.descendants(:block, :if, :for).each do |node|
|
101
|
+
@connections << ConnectedErbNodes.new(
|
102
|
+
node.type,
|
103
|
+
extract_map_locations(node)
|
104
|
+
.map { |loc| find_entry(loc) }
|
105
|
+
.compact.map(&:node)
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
ruby_node.descendants(:kwbegin).each do |node|
|
110
|
+
@connections << ConnectedErbNodes.new(
|
111
|
+
:begin,
|
112
|
+
(extract_map_locations(node) + rescue_locations(node))
|
113
|
+
.map { |loc| find_entry(loc) }
|
114
|
+
.compact.map(&:node)
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
ruby_node.descendants(:case).each do |node|
|
119
|
+
@connections << ConnectedErbNodes.new(
|
120
|
+
node.type,
|
121
|
+
(extract_map_locations(node) + when_locations(node))
|
122
|
+
.map { |loc| find_entry(loc) }
|
123
|
+
.compact.map(&:node)
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
group_overlapping_connections
|
128
|
+
end
|
129
|
+
|
130
|
+
def block?(source)
|
131
|
+
# taken from: action_view/template/handlers/erb/erubi.rb
|
132
|
+
/\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/.match?(source)
|
133
|
+
end
|
134
|
+
|
135
|
+
def when_locations(node)
|
136
|
+
node.child_nodes
|
137
|
+
.select { |child| child.type?(:when) }
|
138
|
+
.map { |child| extract_map_locations(child) }
|
139
|
+
.flatten
|
140
|
+
end
|
141
|
+
|
142
|
+
def rescue_locations(node)
|
143
|
+
node.child_nodes
|
144
|
+
.select { |child| child.type?(:rescue) }
|
145
|
+
.map(&:child_nodes)
|
146
|
+
.flatten
|
147
|
+
.select { |child| child.type?(:resbody) }
|
148
|
+
.map { |child| extract_map_locations(child) }
|
149
|
+
.flatten
|
150
|
+
end
|
151
|
+
|
152
|
+
def extract_map_locations(node)
|
153
|
+
(
|
154
|
+
case node.loc
|
155
|
+
when Parser::Source::Map::Collection
|
156
|
+
[node.loc.begin, node.loc.end]
|
157
|
+
when Parser::Source::Map::Condition
|
158
|
+
[node.loc.keyword, node.loc.begin, node.loc.else, node.loc.end]
|
159
|
+
when Parser::Source::Map::Constant
|
160
|
+
[node.loc.double_colon, node.loc.name, node.loc.operator]
|
161
|
+
when Parser::Source::Map::Definition
|
162
|
+
[node.loc.keyword, node.loc.operator, node.loc.name, node.loc.end]
|
163
|
+
when Parser::Source::Map::For
|
164
|
+
[node.loc.keyword, node.loc.in, node.loc.begin, node.loc.end]
|
165
|
+
when Parser::Source::Map::Heredoc
|
166
|
+
[node.loc.heredoc_body, node.loc.heredoc_end]
|
167
|
+
when Parser::Source::Map::Keyword
|
168
|
+
[node.loc.keyword, node.loc.begin, node.loc.end]
|
169
|
+
when Parser::Source::Map::ObjcKwarg
|
170
|
+
[node.loc.keyword, node.loc.operator, node.loc.argument]
|
171
|
+
when Parser::Source::Map::RescueBody
|
172
|
+
[node.loc.keyword, node.loc.assoc, node.loc.begin]
|
173
|
+
when Parser::Source::Map::Send
|
174
|
+
[node.loc.dot, node.loc.selector, node.loc.operator, node.loc.begin, node.loc.end]
|
175
|
+
when Parser::Source::Map::Ternary
|
176
|
+
[node.loc.question, node.loc.colon]
|
177
|
+
when Parser::Source::Map::Variable
|
178
|
+
[node.loc.name, node.loc.operator]
|
179
|
+
end + [node.loc.expression]
|
180
|
+
).compact
|
181
|
+
end
|
182
|
+
|
183
|
+
def current_pos
|
184
|
+
@ruby_code.size
|
185
|
+
end
|
186
|
+
|
187
|
+
def append(code)
|
188
|
+
@ruby_code += code
|
189
|
+
end
|
190
|
+
|
191
|
+
def parser
|
192
|
+
@processed_source.parser
|
193
|
+
end
|
194
|
+
|
195
|
+
def find_entry(range)
|
196
|
+
return unless range
|
197
|
+
@entries.find do |entry|
|
198
|
+
entry.contains_ruby_range?(Range.new(range.begin_pos, range.end_pos))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def group_overlapping_connections
|
203
|
+
loop do
|
204
|
+
first, second = find_overlapping_pair
|
205
|
+
break unless first && second
|
206
|
+
|
207
|
+
@connections.delete(second)
|
208
|
+
first.concat(second)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def find_overlapping_pair
|
213
|
+
@connections.each do |first|
|
214
|
+
@connections.each do |second|
|
215
|
+
next if first == second
|
216
|
+
return [first, second] if (first & second).any?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
nil
|
220
|
+
end
|
221
|
+
|
222
|
+
def erb_ast
|
223
|
+
parser.ast
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Utils
|
5
|
+
class OffsetCorrector
|
6
|
+
def initialize(processed_source, corrector, offset, bound_range)
|
7
|
+
@processed_source = processed_source
|
8
|
+
@corrector = corrector
|
9
|
+
@offset = offset
|
10
|
+
@bound_range = bound_range
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove(range)
|
14
|
+
@corrector.remove(range_with_offset(range))
|
15
|
+
end
|
16
|
+
|
17
|
+
def insert_before(range, content)
|
18
|
+
@corrector.insert_before(range_with_offset(range), content)
|
19
|
+
end
|
20
|
+
|
21
|
+
def insert_after(range, content)
|
22
|
+
@corrector.insert_after(range_with_offset(range), content)
|
23
|
+
end
|
24
|
+
|
25
|
+
def replace(range, content)
|
26
|
+
@corrector.replace(range_with_offset(range), content)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_preceding(range, size)
|
30
|
+
@corrector.remove_preceding(range_with_offset(range), size)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_leading(range, size)
|
34
|
+
@corrector.remove_leading(range_with_offset(range), size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_trailing(range, size)
|
38
|
+
@corrector.remove_trailing(range_with_offset(range), size)
|
39
|
+
end
|
40
|
+
|
41
|
+
def range_with_offset(range)
|
42
|
+
@processed_source.to_source_range(
|
43
|
+
bound(@offset + range.begin_pos),
|
44
|
+
bound(@offset + range.end_pos - 1),
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def bound(pos)
|
49
|
+
[
|
50
|
+
[pos, @bound_range.begin_pos].max,
|
51
|
+
@bound_range.end_pos - 1
|
52
|
+
].min
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ERBLint
|
4
|
+
module Utils
|
5
|
+
class RubyToERB
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def html_options_to_tag_attributes(hash_node)
|
10
|
+
hash_node.children.map do |pair_node|
|
11
|
+
key_node, value_node = *pair_node
|
12
|
+
key = ruby_to_erb(key_node, '=') { |s| s.tr('_', '-') }
|
13
|
+
value = ruby_to_erb(value_node, '=') { |s| escape_quote(s) }
|
14
|
+
[key, "\"#{value}\""].join('=')
|
15
|
+
end.join(' ')
|
16
|
+
end
|
17
|
+
|
18
|
+
def ruby_to_erb(node, indicator = nil, &block)
|
19
|
+
return node if node.nil? || node.is_a?(String)
|
20
|
+
case node.type
|
21
|
+
when :str, :sym
|
22
|
+
s = node.children.first.to_s
|
23
|
+
s = yield s if block_given?
|
24
|
+
s
|
25
|
+
when :true, :false
|
26
|
+
node.type.to_s
|
27
|
+
when :nil
|
28
|
+
""
|
29
|
+
when :dstr
|
30
|
+
node.children.map do |child|
|
31
|
+
case child.type
|
32
|
+
when :str
|
33
|
+
ruby_to_erb(child, indicator, &block)
|
34
|
+
when :begin
|
35
|
+
ruby_to_erb(child.children.first, indicator, &block)
|
36
|
+
else
|
37
|
+
raise Error, "unexpected #{child.type} in :dstr node"
|
38
|
+
end
|
39
|
+
end.join
|
40
|
+
else
|
41
|
+
"<%#{indicator} #{node.loc.expression.source} %>"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def escape_quote(str)
|
46
|
+
str.gsub('"', '"')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/erb_lint/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erb_lint
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Chan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-01-
|
11
|
+
date: 2018-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: better_html
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.0.
|
19
|
+
version: 1.0.4
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.0.
|
26
|
+
version: 1.0.4
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: html_tokenizer
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,17 +138,24 @@ files:
|
|
138
138
|
- lib/erb_lint/linter.rb
|
139
139
|
- lib/erb_lint/linter_config.rb
|
140
140
|
- lib/erb_lint/linter_registry.rb
|
141
|
+
- lib/erb_lint/linters/allowed_script_type.rb
|
141
142
|
- lib/erb_lint/linters/deprecated_classes.rb
|
142
143
|
- lib/erb_lint/linters/erb_safety.rb
|
143
144
|
- lib/erb_lint/linters/final_newline.rb
|
144
145
|
- lib/erb_lint/linters/hard_coded_string.rb
|
146
|
+
- lib/erb_lint/linters/no_javascript_tag_helper.rb
|
147
|
+
- lib/erb_lint/linters/parser_errors.rb
|
148
|
+
- lib/erb_lint/linters/right_trim.rb
|
145
149
|
- lib/erb_lint/linters/rubocop.rb
|
146
150
|
- lib/erb_lint/linters/rubocop_text.rb
|
151
|
+
- lib/erb_lint/linters/space_around_erb_tag.rb
|
147
152
|
- lib/erb_lint/offense.rb
|
148
|
-
- lib/erb_lint/offset_corrector.rb
|
149
153
|
- lib/erb_lint/processed_source.rb
|
150
154
|
- lib/erb_lint/runner.rb
|
151
155
|
- lib/erb_lint/runner_config.rb
|
156
|
+
- lib/erb_lint/utils/block_map.rb
|
157
|
+
- lib/erb_lint/utils/offset_corrector.rb
|
158
|
+
- lib/erb_lint/utils/ruby_to_erb.rb
|
152
159
|
- lib/erb_lint/version.rb
|
153
160
|
homepage: https://github.com/justinthec/erb-lint
|
154
161
|
licenses:
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ERBLint
|
4
|
-
class OffsetCorrector
|
5
|
-
def initialize(processed_source, corrector, offset, bound_range)
|
6
|
-
@processed_source = processed_source
|
7
|
-
@corrector = corrector
|
8
|
-
@offset = offset
|
9
|
-
@bound_range = bound_range
|
10
|
-
end
|
11
|
-
|
12
|
-
def remove(range)
|
13
|
-
@corrector.remove(range_with_offset(range))
|
14
|
-
end
|
15
|
-
|
16
|
-
def insert_before(range, content)
|
17
|
-
@corrector.insert_before(range_with_offset(range), content)
|
18
|
-
end
|
19
|
-
|
20
|
-
def insert_after(range, content)
|
21
|
-
@corrector.insert_after(range_with_offset(range), content)
|
22
|
-
end
|
23
|
-
|
24
|
-
def replace(range, content)
|
25
|
-
@corrector.replace(range_with_offset(range), content)
|
26
|
-
end
|
27
|
-
|
28
|
-
def remove_preceding(range, size)
|
29
|
-
@corrector.remove_preceding(range_with_offset(range), size)
|
30
|
-
end
|
31
|
-
|
32
|
-
def remove_leading(range, size)
|
33
|
-
@corrector.remove_leading(range_with_offset(range), size)
|
34
|
-
end
|
35
|
-
|
36
|
-
def remove_trailing(range, size)
|
37
|
-
@corrector.remove_trailing(range_with_offset(range), size)
|
38
|
-
end
|
39
|
-
|
40
|
-
def range_with_offset(range)
|
41
|
-
@processed_source.to_source_range(
|
42
|
-
bound(@offset + range.begin_pos),
|
43
|
-
bound(@offset + range.end_pos - 1),
|
44
|
-
)
|
45
|
-
end
|
46
|
-
|
47
|
-
def bound(pos)
|
48
|
-
[
|
49
|
-
[pos, @bound_range.begin_pos].max,
|
50
|
-
@bound_range.end_pos - 1
|
51
|
-
].min
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|