erb_lint 0.0.18 → 0.0.19

Sign up to get free protection for your applications and to get access to all the features.
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