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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0ae1dbd574b00046985466aacce7a126e9a61b34
4
- data.tar.gz: 1649ae3b899e23ee9c02f5f40dfd6a9db02e2008
3
+ metadata.gz: d2a9562a665d609771d10b87e821c7d71a312882
4
+ data.tar.gz: 47b5284105cc9c12311ce4233b7306dc5c8b72e9
5
5
  SHA512:
6
- metadata.gz: d6571d993f2acc94689f7711adb041dbca684b696f22548d85ed5af44a530143adf0acca04de2f19b46313223fbdeb8a3d539ac75827fd07d010c9280ef809d1
7
- data.tar.gz: 1bd62fde864706db8442b6f03290ab71dcf4e94571aee526c425781469f5010f4d78ce8091cae3264f3a4d8927d5f4584f3d75c5770e056458f3f55decc4a69a
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'better_html'
3
4
  require 'better_html/parser'
4
5
 
5
6
  module ERBLint
@@ -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
- tester = Tester.new(processed_source.file_content, config: better_html_config)
27
- tester.errors.each do |error|
28
- offenses << Offense.new(
29
- self,
30
- processed_source.to_source_range(error.location.start, error.location.stop),
31
- error.message
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
- offenses = []
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.correction
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.each do |cop|
84
- cop.offenses.select(&:corrected?).each_with_index do |rubocop_offense, index|
85
- correction = cop.corrections[index] if rubocop_offense.corrected?
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.delete('inherit_from')
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 team
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::Team.new(cop_classes, @rubocop_config, extra_details: true, display_cop_names: true)
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
@@ -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
- enabled: true,
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('"', '&quot;')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ERBLint
4
- VERSION = '0.0.18'
4
+ VERSION = '0.0.19'
5
5
  end
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.18
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-22 00:00:00.000000000 Z
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.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.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