erb_lint 0.0.34 → 0.1.0

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
  SHA256:
3
- metadata.gz: 65e35ea36e2654316241c7267aca2f9b581fa7eb5c858008a80dfc098c07a187
4
- data.tar.gz: db35c2f60f85651728bec14a853fc8d7060e93e686c9cababa55bccb3dd46c49
3
+ metadata.gz: 5c54cf59770c0719d8cae0376d27a685ba2159d0e8884c00a472b0217d1ca2e6
4
+ data.tar.gz: c644e23a805c04e02daf4218f8548a167a90cc8d0b2bf0fd73acbbdbbaec237c
5
5
  SHA512:
6
- metadata.gz: dbe12716e085e0baa6beef5c1395bf86874f442db6087c2d1312315cb4d60704cca5a007a3843738197d58fa0025d35b993bbbed5606ca9a3c71b81928d365e2
7
- data.tar.gz: 7bb3506036e3a098cb9c688c20373a80bc413a6528fcb05b951e16996dd62eea358c01658f5b2aceb4d4b1b4bc63afa7ad53d852478b2b49d549a13f3da6ed1c
6
+ metadata.gz: '07718e5eed2bfb8246a529615c3950645c0d51e376475c6cea7eb60bedfc17af42c4f24f3aba7e8184e842087027b3c0e033afb6c1d01eceb5b84e7c0da8bb74'
7
+ data.tar.gz: 6bda3b3ad2b03a0812a6bb796c0f6f2d03545fcdc44a6130dc7e3b2135d523e8decb395e3fd7599d9121045c840585de3c4ced6c9c8fb642d1865d263e978dad
data/lib/erb_lint.rb CHANGED
@@ -1,17 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb_lint/corrector'
4
- require 'erb_lint/file_loader'
5
- require 'erb_lint/linter_config'
6
- require 'erb_lint/linter_registry'
7
- require 'erb_lint/linter'
8
- require 'erb_lint/offense'
9
- require 'erb_lint/processed_source'
10
- require 'erb_lint/runner_config'
11
- require 'erb_lint/runner'
12
3
  require 'erb_lint/version'
13
-
14
- # Load linters
15
- Dir[File.expand_path('erb_lint/linters/**/*.rb', File.dirname(__FILE__))].each do |file|
16
- require file
17
- end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ require 'erb_lint'
6
+ require 'erb_lint/corrector'
7
+ require 'erb_lint/file_loader'
8
+ require 'erb_lint/linter_config'
9
+ require 'erb_lint/linter_registry'
10
+ require 'erb_lint/linter'
11
+ require 'erb_lint/offense'
12
+ require 'erb_lint/processed_source'
13
+ require 'erb_lint/runner_config'
14
+ require 'erb_lint/runner'
15
+ require 'erb_lint/stats'
16
+ require 'erb_lint/reporter'
17
+
18
+ # Load linters
19
+ Dir[File.expand_path('linters/**/*.rb', __dir__)].each do |file|
20
+ require file
21
+ end
22
+
23
+ # Load reporters
24
+ Dir[File.expand_path('reporters/**/*.rb', __dir__)].each do |file|
25
+ require file
26
+ end
data/lib/erb_lint/cli.rb CHANGED
@@ -1,29 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb_lint'
3
+ require 'erb_lint/all'
4
4
  require 'active_support'
5
5
  require 'active_support/inflector'
6
6
  require 'optparse'
7
7
  require 'psych'
8
8
  require 'yaml'
9
9
  require 'rainbow'
10
+ require 'erb_lint/utils/severity_levels'
10
11
 
11
12
  module ERBLint
12
13
  class CLI
14
+ include Utils::SeverityLevels
15
+
13
16
  DEFAULT_CONFIG_FILENAME = '.erb-lint.yml'
14
17
  DEFAULT_LINT_ALL_GLOB = "**/*.html{+*,}.erb"
15
18
 
16
19
  class ExitWithFailure < RuntimeError; end
17
- class ExitWithSuccess < RuntimeError; end
18
20
 
19
- class Stats
20
- attr_accessor :found, :corrected, :exceptions
21
- def initialize
22
- @found = 0
23
- @corrected = 0
24
- @exceptions = 0
25
- end
26
- end
21
+ class ExitWithSuccess < RuntimeError; end
27
22
 
28
23
  def initialize
29
24
  @options = {}
@@ -35,7 +30,7 @@ module ERBLint
35
30
  def run(args = ARGV)
36
31
  dupped_args = args.dup
37
32
  load_options(dupped_args)
38
- @files = dupped_args
33
+ @files = @options[:stdin] || dupped_args
39
34
 
40
35
  load_config
41
36
 
@@ -51,19 +46,24 @@ module ERBLint
51
46
  failure!('no linter available with current configuration')
52
47
  end
53
48
 
54
- puts "Linting #{lint_files.size} files with "\
55
- "#{enabled_linter_classes.size} #{'autocorrectable ' if autocorrect?}linters..."
56
- puts
49
+ @options[:format] ||= :multiline
50
+ @options[:fail_level] ||= severity_level_for_name(:refactor)
51
+ @stats.files = lint_files.size
52
+ @stats.linters = enabled_linter_classes.size
53
+
54
+ reporter = Reporter.create_reporter(@options[:format], @stats, autocorrect?)
55
+ reporter.preview
57
56
 
58
57
  runner = ERBLint::Runner.new(file_loader, @config)
58
+ file_content = nil
59
59
 
60
60
  lint_files.each do |filename|
61
61
  runner.clear_offenses
62
62
  begin
63
- run_with_corrections(runner, filename)
63
+ file_content = run_with_corrections(runner, filename)
64
64
  rescue => e
65
65
  @stats.exceptions += 1
66
- puts "Exception occured when processing: #{relative_filename(filename)}"
66
+ puts "Exception occurred when processing: #{relative_filename(filename)}"
67
67
  puts "If this file cannot be processed by erb-lint, "\
68
68
  "you can exclude it in your configuration file."
69
69
  puts e.message
@@ -72,19 +72,12 @@ module ERBLint
72
72
  end
73
73
  end
74
74
 
75
- if @stats.corrected > 0
76
- corrected_found_diff = @stats.found - @stats.corrected
77
- if corrected_found_diff > 0
78
- warn(Rainbow(
79
- "#{@stats.corrected} error(s) corrected and #{corrected_found_diff} error(s) remaining in ERB files"
80
- ).red)
81
- else
82
- puts Rainbow("#{@stats.corrected} error(s) corrected in ERB files").green
83
- end
84
- elsif @stats.found > 0
85
- warn(Rainbow("#{@stats.found} error(s) were found in ERB files").red)
86
- else
87
- puts Rainbow("No errors were found in ERB files").green
75
+ reporter.show
76
+
77
+ if stdin? && autocorrect?
78
+ # When running from stdin, we only lint a single file
79
+ puts "================ #{lint_files.first} ==================\n"
80
+ puts file_content
88
81
  end
89
82
 
90
83
  @stats.found == 0 && @stats.exceptions == 0
@@ -106,7 +99,7 @@ module ERBLint
106
99
  end
107
100
 
108
101
  def run_with_corrections(runner, filename)
109
- file_content = File.read(filename, encoding: Encoding::UTF_8)
102
+ file_content = read_content(filename)
110
103
 
111
104
  7.times do
112
105
  processed_source = ERBLint::ProcessedSource.new(filename, file_content)
@@ -119,22 +112,35 @@ module ERBLint
119
112
 
120
113
  @stats.corrected += corrector.corrections.size
121
114
 
122
- File.open(filename, "wb") do |file|
123
- file.write(corrector.corrected_content)
115
+ # Don't overwrite the file if the input comes from stdin
116
+ unless stdin?
117
+ File.open(filename, "wb") do |file|
118
+ file.write(corrector.corrected_content)
119
+ end
124
120
  end
125
121
 
126
122
  file_content = corrector.corrected_content
127
123
  runner.clear_offenses
128
124
  end
125
+ offenses_filename = relative_filename(filename)
126
+ offenses = runner.offenses || []
129
127
 
130
- @stats.found += runner.offenses.size
131
- runner.offenses.each do |offense|
132
- puts <<~EOF
133
- #{offense.message}#{Rainbow(' (not autocorrected)').red if autocorrect?}
134
- In file: #{relative_filename(filename)}:#{offense.line_range.begin}
128
+ @stats.ignored, @stats.found = offenses.partition do |offense|
129
+ severity_level_for_name(offense.severity) < @options[:fail_level]
130
+ end.map(&:size)
131
+ .zip([@stats.ignored, @stats.found])
132
+ .map(&:sum)
135
133
 
136
- EOF
137
- end
134
+ @stats.processed_files[offenses_filename] ||= []
135
+ @stats.processed_files[offenses_filename] |= offenses
136
+
137
+ file_content
138
+ end
139
+
140
+ def read_content(filename)
141
+ return File.read(filename, encoding: Encoding::UTF_8) unless stdin?
142
+
143
+ $stdin.binmode.read.force_encoding(Encoding::UTF_8)
138
144
  end
139
145
 
140
146
  def correct(processed_source, offenses)
@@ -190,7 +196,8 @@ module ERBLint
190
196
 
191
197
  def excluded?(filename)
192
198
  @config.global_exclude.any? do |path|
193
- File.fnmatch?(path, filename)
199
+ expanded_path = File.expand_path(path, Dir.pwd)
200
+ File.fnmatch?(expanded_path, filename)
194
201
  end
195
202
  end
196
203
 
@@ -258,6 +265,15 @@ module ERBLint
258
265
  end
259
266
  end
260
267
 
268
+ opts.on("--format FORMAT", format_options_help) do |format|
269
+ unless Reporter.available_format?(format)
270
+ error_message = invalid_format_error_message(format)
271
+ failure!(error_message)
272
+ end
273
+
274
+ @options[:format] = format
275
+ end
276
+
261
277
  opts.on("--lint-all", "Lint all files matching configured glob [default: #{DEFAULT_LINT_ALL_GLOB}]") do |config|
262
278
  @options[:lint_all] = config
263
279
  end
@@ -276,10 +292,27 @@ module ERBLint
276
292
  @options[:enabled_linters] = linters
277
293
  end
278
294
 
295
+ opts.on("--fail-level SEVERITY", "Minimum severity for exit with error code") do |level|
296
+ parsed_severity = SEVERITY_CODE_TABLE[level.upcase.to_sym] || (SEVERITY_NAMES & [level.downcase]).first
297
+
298
+ if parsed_severity.nil?
299
+ failure!("#{level}: not a valid failure level (#{SEVERITY_NAMES.join(', ')})")
300
+ end
301
+ @options[:fail_level] = severity_level_for_name(parsed_severity)
302
+ end
303
+
279
304
  opts.on("-a", "--autocorrect", "Correct offenses automatically if possible (default: false)") do |config|
280
305
  @options[:autocorrect] = config
281
306
  end
282
307
 
308
+ opts.on(
309
+ "-sFILE",
310
+ "--stdin FILE",
311
+ "Pipe source from STDIN. Takes the path to be used to check which rules to apply."
312
+ ) do |file|
313
+ @options[:stdin] = [file]
314
+ end
315
+
283
316
  opts.on_tail("-h", "--help", "Show this message") do
284
317
  success!(opts)
285
318
  end
@@ -289,5 +322,19 @@ module ERBLint
289
322
  end
290
323
  end
291
324
  end
325
+
326
+ def format_options_help
327
+ "Report offenses in the given format: "\
328
+ "(#{Reporter.available_formats.join(', ')}) (default: multiline)"
329
+ end
330
+
331
+ def invalid_format_error_message(given_format)
332
+ formats = Reporter.available_formats.map { |format| " - #{format}\n" }
333
+ "#{given_format}: is not a valid format. Available formats:\n#{formats.join}"
334
+ end
335
+
336
+ def stdin?
337
+ @options[:stdin].present?
338
+ end
292
339
  end
293
340
  end
@@ -17,11 +17,22 @@ module ERBLint
17
17
  end
18
18
 
19
19
  def corrector
20
- RuboCop::Cop::Corrector.new(@processed_source.source_buffer, corrections)
20
+ BASE.new(@processed_source.source_buffer, corrections)
21
21
  end
22
22
 
23
- def diagnostics
24
- corrector.diagnostics
23
+ if ::RuboCop::Version::STRING.to_f >= 0.87
24
+ require 'rubocop/cop/legacy/corrector'
25
+ BASE = ::RuboCop::Cop::Legacy::Corrector
26
+
27
+ def diagnostics
28
+ []
29
+ end
30
+ else
31
+ BASE = ::RuboCop::Cop::Corrector
32
+
33
+ def diagnostics
34
+ corrector.diagnostics
35
+ end
25
36
  end
26
37
  end
27
38
  end
@@ -14,6 +14,7 @@ module ERBLint
14
14
  # `ERBLint::Linters::Foo.simple_name` #=> "Foo"
15
15
  # `ERBLint::Linters::Compass::Bar.simple_name` #=> "Compass::Bar"
16
16
  def inherited(linter)
17
+ super
17
18
  linter.simple_name = if linter.name.start_with?('ERBLint::Linters::')
18
19
  name_parts = linter.name.split('::')
19
20
  name_parts[2..-1].join('::')
@@ -52,8 +53,8 @@ module ERBLint
52
53
  raise NotImplementedError, "must implement ##{__method__}"
53
54
  end
54
55
 
55
- def add_offense(source_range, message, context = nil)
56
- @offenses << Offense.new(self, source_range, message, context)
56
+ def add_offense(source_range, message, context = nil, severity = nil)
57
+ @offenses << Offense.new(self, source_range, message, context, severity)
57
58
  end
58
59
 
59
60
  def clear_offenses
@@ -4,19 +4,28 @@ module ERBLint
4
4
  # Stores all linters available to the application.
5
5
  module LinterRegistry
6
6
  CUSTOM_LINTERS_DIR = '.erb-linters'
7
- @linters = []
7
+ @loaded_linters = []
8
8
 
9
9
  class << self
10
- attr_reader :linters
10
+ def clear
11
+ @linters = nil
12
+ end
11
13
 
12
14
  def included(linter_class)
13
- @linters << linter_class
15
+ @loaded_linters << linter_class
14
16
  end
15
17
 
16
18
  def find_by_name(name)
17
19
  linters.detect { |linter| linter.simple_name == name }
18
20
  end
19
21
 
22
+ def linters
23
+ @linters ||= begin
24
+ load_custom_linters
25
+ @loaded_linters
26
+ end
27
+ end
28
+
20
29
  def load_custom_linters(directory = CUSTOM_LINTERS_DIR)
21
30
  ruby_files = Dir.glob(File.expand_path(File.join(directory, '**', '*.rb')))
22
31
  ruby_files.each { |file| require file }
@@ -19,7 +19,27 @@ module ERBLint
19
19
  )
20
20
 
21
21
  NON_TEXT_TAGS = Set.new(%w(script style xmp iframe noembed noframes listing))
22
- BLACK_LISTED_TEXT = Set.new(%w(&nbsp; &ensp; &emsp; &thinsp;))
22
+ BLACK_LISTED_TEXT = Set.new(%w(
23
+ &nbsp;
24
+ &amp;
25
+ &lt;
26
+ &gt;
27
+ &quot;
28
+ &copy;
29
+ &reg;
30
+ &trade;
31
+ &hellip;
32
+ &mdash;
33
+ &bull;
34
+ &ldquo;
35
+ &rdquo;
36
+ &lsquo;
37
+ &rsquo;
38
+ &larr;
39
+ &rarr;
40
+ &darr;
41
+ &uarr;
42
+ ))
23
43
 
24
44
  class ConfigSchema < LinterConfig
25
45
  property :corrector, accepts: Hash, required: false, default: -> { {} }
@@ -63,7 +83,7 @@ module ERBLint
63
83
  string = offense.source_range.source
64
84
  return unless (klass = load_corrector)
65
85
  return unless string.strip.length > 1
66
- node = RuboCop::AST::StrNode.new(:str, [string])
86
+ node = ::RuboCop::AST::StrNode.new(:str, [string])
67
87
  corrector = klass.new(node, processed_source.filename, corrector_i18n_load_path, offense.source_range)
68
88
  corrector.autocorrect(tag_start: '<%= ', tag_end: ' %>')
69
89
  rescue MissingCorrector, MissingI18nLoadPath
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ # Checks for instance variables in partials.
6
+ class PartialInstanceVariable < Linter
7
+ include LinterRegistry
8
+
9
+ def run(processed_source)
10
+ instance_variable_regex = /\s@\w+/
11
+ return unless processed_source.filename.match?(/.*_.*.erb\z/) &&
12
+ processed_source.file_content.match?(instance_variable_regex)
13
+
14
+ add_offense(
15
+ processed_source.to_source_range(
16
+ processed_source.file_content =~ instance_variable_regex..processed_source.file_content.size
17
+ ),
18
+ "Instance variable detected in partial."
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'better_html'
4
+ require 'better_html/tree/tag'
5
+
6
+ module ERBLint
7
+ module Linters
8
+ class RequireInputAutocomplete < Linter
9
+ include LinterRegistry
10
+
11
+ HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE = [
12
+ "color",
13
+ "date",
14
+ "datetime-local",
15
+ "email",
16
+ "hidden",
17
+ "month",
18
+ "number",
19
+ "password",
20
+ "range",
21
+ "search",
22
+ "tel",
23
+ "text",
24
+ "time",
25
+ "url",
26
+ "week",
27
+ ].freeze
28
+
29
+ FORM_HELPERS_REQUIRING_AUTOCOMPLETE = [
30
+ :date_field_tag,
31
+ :color_field_tag,
32
+ :email_field_tag,
33
+ :text_field_tag,
34
+ :utf8_enforcer_tag,
35
+ :month_field_tag,
36
+ :hidden_field_tag,
37
+ :number_field_tag,
38
+ :password_field_tag,
39
+ :search_field_tag,
40
+ :telephone_field_tag,
41
+ :time_field_tag,
42
+ :url_field_tag,
43
+ :week_field_tag,
44
+ ].freeze
45
+
46
+ def run(processed_source)
47
+ parser = processed_source.parser
48
+
49
+ find_html_input_tags(parser)
50
+ find_rails_helper_input_tags(parser)
51
+ end
52
+
53
+ private
54
+
55
+ def find_html_input_tags(parser)
56
+ parser.nodes_with_type(:tag).each do |tag_node|
57
+ tag = BetterHtml::Tree::Tag.from_node(tag_node)
58
+
59
+ autocomplete_attribute = tag.attributes['autocomplete']
60
+ type_attribute = tag.attributes['type']
61
+
62
+ next if !html_input_tag?(tag) || autocomplete_present?(autocomplete_attribute)
63
+ next unless html_type_requires_autocomplete_attribute?(type_attribute)
64
+
65
+ add_offense(
66
+ tag_node.to_a[1].loc,
67
+ "Input tag is missing an autocomplete attribute. If no "\
68
+ "autocomplete behaviour is desired, use the value `off` or `nope`.",
69
+ [autocomplete_attribute]
70
+ )
71
+ end
72
+ end
73
+
74
+ def autocomplete_present?(autocomplete_attribute)
75
+ autocomplete_attribute.present? && autocomplete_attribute.value_node.present?
76
+ end
77
+
78
+ def html_input_tag?(tag)
79
+ !tag.closing? && tag.name == 'input'
80
+ end
81
+
82
+ def html_type_requires_autocomplete_attribute?(type_attribute)
83
+ type_present = type_attribute.present? && type_attribute.value_node.present?
84
+ type_present && HTML_INPUT_TYPES_REQUIRING_AUTOCOMPLETE.include?(type_attribute.value)
85
+ end
86
+
87
+ def find_rails_helper_input_tags(parser)
88
+ parser.ast.descendants(:erb).each do |erb_node|
89
+ indicator_node, _, code_node, _ = *erb_node
90
+ source = code_node.loc.source
91
+ ruby_node = extract_ruby_node(source)
92
+ send_node = ruby_node&.descendants(:send)&.first
93
+
94
+ next if code_comment?(indicator_node) ||
95
+ !ruby_node ||
96
+ !input_helper?(send_node) ||
97
+ source.include?("autocomplete")
98
+
99
+ add_offense(
100
+ erb_node.loc,
101
+ "Input field helper is missing an autocomplete attribute. If no "\
102
+ "autocomplete behaviour is desired, use the value `off` or `nope`.",
103
+ [erb_node, send_node]
104
+ )
105
+ end
106
+ end
107
+
108
+ def input_helper?(send_node)
109
+ FORM_HELPERS_REQUIRING_AUTOCOMPLETE.include?(send_node&.method_name)
110
+ end
111
+
112
+ def code_comment?(indicator_node)
113
+ indicator_node&.loc&.source == '#'
114
+ end
115
+
116
+ def extract_ruby_node(source)
117
+ BetterHtml::TestHelper::RubyNode.parse(source)
118
+ rescue ::Parser::SyntaxError
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'better_html'
4
+ require 'better_html/tree/tag'
5
+
6
+ module ERBLint
7
+ module Linters
8
+ # Allow inline script tags in ERB that have a nonce attribute.
9
+ # This only validates inline <script> tags, as well as rails helpers like javascript_tag.
10
+ class RequireScriptNonce < Linter
11
+ include LinterRegistry
12
+
13
+ def run(processed_source)
14
+ parser = processed_source.parser
15
+
16
+ find_html_script_tags(parser)
17
+ find_rails_helper_script_tags(parser)
18
+ end
19
+
20
+ private
21
+
22
+ def find_html_script_tags(parser)
23
+ parser.nodes_with_type(:tag).each do |tag_node|
24
+ tag = BetterHtml::Tree::Tag.from_node(tag_node)
25
+ nonce_attribute = tag.attributes['nonce']
26
+
27
+ next if !html_javascript_tag?(tag) || nonce_present?(nonce_attribute)
28
+
29
+ add_offense(
30
+ tag_node.to_a[1].loc,
31
+ "Missing a nonce attribute. Use request.content_security_policy_nonce",
32
+ [nonce_attribute]
33
+ )
34
+ end
35
+ end
36
+
37
+ def nonce_present?(nonce_attribute)
38
+ nonce_attribute.present? && nonce_attribute.value_node.present?
39
+ end
40
+
41
+ def html_javascript_tag?(tag)
42
+ !tag.closing? &&
43
+ (tag.name == 'script' && !html_javascript_type_attribute?(tag))
44
+ end
45
+
46
+ def html_javascript_type_attribute?(tag)
47
+ type_attribute = tag.attributes['type']
48
+
49
+ type_attribute &&
50
+ type_attribute.value_node.present? &&
51
+ type_attribute.value_node.to_a[1] != 'text/javascript' &&
52
+ type_attribute.value_node.to_a[1] != 'application/javascript'
53
+ end
54
+
55
+ def find_rails_helper_script_tags(parser)
56
+ parser.ast.descendants(:erb).each do |erb_node|
57
+ indicator_node, _, code_node, _ = *erb_node
58
+ source = code_node.loc.source
59
+ ruby_node = extract_ruby_node(source)
60
+ send_node = ruby_node&.descendants(:send)&.first
61
+
62
+ next if code_comment?(indicator_node) ||
63
+ !ruby_node ||
64
+ !tag_helper?(send_node) ||
65
+ source.include?("nonce")
66
+
67
+ add_offense(
68
+ erb_node.loc,
69
+ "Missing a nonce attribute. Use nonce: true",
70
+ [erb_node, send_node]
71
+ )
72
+ end
73
+ end
74
+
75
+ def tag_helper?(send_node)
76
+ send_node&.method_name?(:javascript_tag) ||
77
+ send_node&.method_name?(:javascript_include_tag) ||
78
+ send_node&.method_name?(:javascript_pack_tag)
79
+ end
80
+
81
+ def code_comment?(indicator_node)
82
+ indicator_node&.loc&.source == '#'
83
+ end
84
+
85
+ def extract_ruby_node(source)
86
+ BetterHtml::TestHelper::RubyNode.parse(source)
87
+ rescue ::Parser::SyntaxError
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'better_html'
4
- require 'rubocop'
5
4
  require 'tempfile'
6
5
  require 'erb_lint/utils/offset_corrector'
7
6
 
@@ -26,7 +25,7 @@ module ERBLint
26
25
  super
27
26
  @only_cops = @config.only
28
27
  custom_config = config_from_hash(@config.rubocop_config)
29
- @rubocop_config = RuboCop::ConfigLoader.merge_with_default(custom_config, '')
28
+ @rubocop_config = ::RuboCop::ConfigLoader.merge_with_default(custom_config, '')
30
29
  end
31
30
 
32
31
  def run(processed_source)
@@ -35,17 +34,29 @@ module ERBLint
35
34
  end
36
35
  end
37
36
 
38
- def autocorrect(processed_source, offense)
39
- return unless offense.context
40
-
41
- lambda do |corrector|
42
- passthrough = Utils::OffsetCorrector.new(
43
- processed_source,
44
- corrector,
45
- offense.context[:offset],
46
- offense.context[:bound_range],
47
- )
48
- offense.context[:rubocop_correction].call(passthrough)
37
+ if ::RuboCop::Version::STRING.to_f >= 0.87
38
+ def autocorrect(_processed_source, offense)
39
+ return unless offense.context
40
+ rubocop_correction = offense.context[:rubocop_correction]
41
+ return unless rubocop_correction
42
+
43
+ lambda do |corrector|
44
+ corrector.import!(rubocop_correction, offset: offense.context[:offset])
45
+ end
46
+ end
47
+ else
48
+ def autocorrect(processed_source, offense)
49
+ return unless offense.context
50
+
51
+ lambda do |corrector|
52
+ passthrough = Utils::OffsetCorrector.new(
53
+ processed_source,
54
+ corrector,
55
+ offense.context[:offset],
56
+ offense.context[:bound_range],
57
+ )
58
+ offense.context[:rubocop_correction].call(passthrough)
59
+ end
49
60
  end
50
61
  end
51
62
 
@@ -62,22 +73,23 @@ module ERBLint
62
73
  original_source = code_node.loc.source
63
74
  trimmed_source = original_source.sub(BLOCK_EXPR, '').sub(SUFFIX_EXPR, '')
64
75
  alignment_column = code_node.loc.column
76
+ offset = code_node.loc.begin_pos - alignment_column
65
77
  aligned_source = "#{' ' * alignment_column}#{trimmed_source}"
66
78
 
67
79
  source = rubocop_processed_source(aligned_source, processed_source.filename)
68
80
  return unless source.valid_syntax?
69
81
 
70
- team = build_team
71
- team.inspect_file(source)
72
- team.cops.each do |cop|
73
- correction_offset = 0
74
- cop.offenses.reject(&:disabled?).each do |rubocop_offense|
75
- if rubocop_offense.corrected?
76
- correction = cop.corrections[correction_offset]
77
- correction_offset += 1
78
- end
82
+ activate_team(processed_source, source, offset, code_node, build_team)
83
+ end
84
+
85
+ if ::RuboCop::Version::STRING.to_f >= 0.87
86
+ def activate_team(processed_source, source, offset, code_node, team)
87
+ report = team.investigate(source)
88
+ report.offenses.each do |rubocop_offense|
89
+ next if rubocop_offense.disabled?
90
+
91
+ correction = rubocop_offense.corrector if rubocop_offense.corrected?
79
92
 
80
- offset = code_node.loc.begin_pos - alignment_column
81
93
  offense_range = processed_source
82
94
  .to_source_range(rubocop_offense.location)
83
95
  .offset(offset)
@@ -85,6 +97,25 @@ module ERBLint
85
97
  add_offense(rubocop_offense, offense_range, correction, offset, code_node.loc.range)
86
98
  end
87
99
  end
100
+ else
101
+ def activate_team(processed_source, source, offset, code_node, team)
102
+ team.inspect_file(source)
103
+ team.cops.each do |cop|
104
+ correction_offset = 0
105
+ cop.offenses.reject(&:disabled?).each do |rubocop_offense|
106
+ if rubocop_offense.corrected?
107
+ correction = cop.corrections[correction_offset]
108
+ correction_offset += 1
109
+ end
110
+
111
+ offense_range = processed_source
112
+ .to_source_range(rubocop_offense.location)
113
+ .offset(offset)
114
+
115
+ add_offense(rubocop_offense, offense_range, correction, offset, code_node.loc.range)
116
+ end
117
+ end
118
+ end
88
119
  end
89
120
 
90
121
  def tempfile_from(filename, content)
@@ -97,7 +128,7 @@ module ERBLint
97
128
  end
98
129
 
99
130
  def rubocop_processed_source(content, filename)
100
- RuboCop::ProcessedSource.new(
131
+ ::RuboCop::ProcessedSource.new(
101
132
  content,
102
133
  @rubocop_config.target_ruby_version,
103
134
  filename
@@ -106,15 +137,15 @@ module ERBLint
106
137
 
107
138
  def cop_classes
108
139
  if @only_cops.present?
109
- selected_cops = RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
110
- RuboCop::Cop::Registry.new(selected_cops)
140
+ selected_cops = ::RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
141
+ ::RuboCop::Cop::Registry.new(selected_cops)
111
142
  else
112
- RuboCop::Cop::Registry.new(RuboCop::Cop::Cop.all)
143
+ ::RuboCop::Cop::Registry.new(::RuboCop::Cop::Cop.all)
113
144
  end
114
145
  end
115
146
 
116
147
  def build_team
117
- RuboCop::Cop::Team.new(
148
+ ::RuboCop::Cop::Team.new(
118
149
  cop_classes,
119
150
  @rubocop_config,
120
151
  extra_details: true,
@@ -129,7 +160,7 @@ module ERBLint
129
160
  resolve_inheritance(hash, inherit_from)
130
161
 
131
162
  tempfile_from('.erblint-rubocop', hash.to_yaml) do |tempfile|
132
- RuboCop::ConfigLoader.load_file(tempfile.path)
163
+ ::RuboCop::ConfigLoader.load_file(tempfile.path)
133
164
  end
134
165
  end
135
166
 
@@ -137,7 +168,7 @@ module ERBLint
137
168
  base_configs(inherit_from)
138
169
  .reverse_each do |base_config|
139
170
  base_config.each do |k, v|
140
- hash[k] = hash.key?(k) ? RuboCop::ConfigLoader.merge(v, hash[k]) : v if v.is_a?(Hash)
171
+ hash[k] = hash.key?(k) ? ::RuboCop::ConfigLoader.merge(v, hash[k]) : v if v.is_a?(Hash)
141
172
  end
142
173
  end
143
174
  end
@@ -146,7 +177,7 @@ module ERBLint
146
177
  regex = URI::DEFAULT_PARSER.make_regexp(%w(http https))
147
178
  configs = Array(inherit_from).compact.map do |base_name|
148
179
  if base_name =~ /\A#{regex}\z/
149
- RuboCop::ConfigLoader.load_file(RuboCop::RemoteConfig.new(base_name, Dir.pwd))
180
+ ::RuboCop::ConfigLoader.load_file(::RuboCop::RemoteConfig.new(base_name, Dir.pwd))
150
181
  else
151
182
  config_from_hash(@file_loader.yaml(base_name))
152
183
  end
@@ -160,7 +191,7 @@ module ERBLint
160
191
  { rubocop_correction: correction, offset: offset, bound_range: bound_range }
161
192
  end
162
193
 
163
- super(offense_range, rubocop_offense.message.strip, context)
194
+ super(offense_range, rubocop_offense.message.strip, context, rubocop_offense.severity.name)
164
195
  end
165
196
  end
166
197
  end
@@ -28,9 +28,9 @@ module ERBLint
28
28
  end
29
29
 
30
30
  def cop_classes
31
- selected_cops = RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
31
+ selected_cops = ::RuboCop::Cop::Cop.all.select { |cop| cop.match?(@only_cops) }
32
32
 
33
- RuboCop::Cop::Registry.new(selected_cops)
33
+ ::RuboCop::Cop::Registry.new(selected_cops)
34
34
  end
35
35
  end
36
36
  end
@@ -3,9 +3,9 @@
3
3
  module ERBLint
4
4
  # Defines common functionality available to all linters.
5
5
  class Offense
6
- attr_reader :linter, :source_range, :message, :context
6
+ attr_reader :linter, :source_range, :message, :context, :severity
7
7
 
8
- def initialize(linter, source_range, message, context = nil)
8
+ def initialize(linter, source_range, message, context = nil, severity = 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
@@ -13,23 +13,34 @@ module ERBLint
13
13
  @source_range = source_range
14
14
  @message = message
15
15
  @context = context
16
+ @severity = severity
16
17
  end
17
18
 
18
19
  def inspect
19
20
  "#<#{self.class.name} linter=#{linter.class.name} "\
20
21
  "source_range=#{source_range.begin_pos}...#{source_range.end_pos} "\
21
- "message=#{message}>"
22
+ "message=#{message}> "\
23
+ "severity=#{severity}"
22
24
  end
23
25
 
24
26
  def ==(other)
25
27
  other.class <= ERBLint::Offense &&
26
28
  other.linter == linter &&
27
29
  other.source_range == source_range &&
28
- other.message == message
30
+ other.message == message &&
31
+ other.severity == severity
29
32
  end
30
33
 
31
34
  def line_range
32
35
  Range.new(source_range.line, source_range.last_line)
33
36
  end
37
+
38
+ def line_number
39
+ line_range.begin
40
+ end
41
+
42
+ def column
43
+ source_range.column
44
+ end
34
45
  end
35
46
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/class'
3
+ require 'active_support/core_ext/module/delegation'
4
+
5
+ module ERBLint
6
+ class Reporter
7
+ def self.create_reporter(format, *args)
8
+ reporter_klass = "#{ERBLint::Reporters}::#{format.to_s.camelize}Reporter".constantize
9
+ reporter_klass.new(*args)
10
+ end
11
+
12
+ def self.available_format?(format)
13
+ available_formats.include?(format.to_s)
14
+ end
15
+
16
+ def self.available_formats
17
+ descendants
18
+ .map(&:to_s)
19
+ .map(&:demodulize)
20
+ .map(&:underscore)
21
+ .map { |klass_name| klass_name.sub("_reporter", "") }
22
+ .sort
23
+ end
24
+
25
+ def initialize(stats, autocorrect)
26
+ @stats = stats
27
+ @autocorrect = autocorrect
28
+ end
29
+
30
+ def preview; end
31
+
32
+ def show; end
33
+
34
+ private
35
+
36
+ attr_reader :stats, :autocorrect
37
+ delegate :processed_files, to: :stats
38
+ end
39
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Reporters
5
+ class CompactReporter < Reporter
6
+ def preview
7
+ puts "Linting #{stats.files} files with "\
8
+ "#{stats.linters} #{'autocorrectable ' if autocorrect}linters..."
9
+ end
10
+
11
+ def show
12
+ processed_files.each do |filename, offenses|
13
+ offenses.each do |offense|
14
+ puts format_offense(filename, offense)
15
+ end
16
+ end
17
+
18
+ footer
19
+ summary
20
+ end
21
+
22
+ private
23
+
24
+ def format_offense(filename, offense)
25
+ [
26
+ "#{filename}:",
27
+ "#{offense.line_number}:",
28
+ "#{offense.column}: ",
29
+ offense.message.to_s,
30
+ ].join
31
+ end
32
+
33
+ def footer; end
34
+
35
+ def summary
36
+ if stats.corrected > 0
37
+ report_corrected_offenses
38
+ elsif stats.ignored > 0 || stats.found > 0
39
+ if stats.ignored > 0
40
+ warn(Rainbow("#{stats.ignored} error(s) were ignored in ERB files").yellow)
41
+ end
42
+
43
+ if stats.found > 0
44
+ warn(Rainbow("#{stats.found} error(s) were found in ERB files").red)
45
+ end
46
+ else
47
+ puts Rainbow("No errors were found in ERB files").green
48
+ end
49
+ end
50
+
51
+ def report_corrected_offenses
52
+ corrected_found_diff = stats.found - stats.corrected
53
+
54
+ if corrected_found_diff > 0
55
+ message = Rainbow(
56
+ "#{stats.corrected} error(s) corrected and #{corrected_found_diff} error(s) remaining in ERB files"
57
+ ).red
58
+
59
+ warn(message)
60
+ else
61
+ puts Rainbow("#{stats.corrected} error(s) corrected in ERB files").green
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ERBLint
6
+ module Reporters
7
+ class JsonReporter < Reporter
8
+ def preview; end
9
+
10
+ def show
11
+ puts formatted_data
12
+ end
13
+
14
+ private
15
+
16
+ def formatted_data
17
+ {
18
+ metadata: metadata,
19
+ files: formatted_files,
20
+ summary: summary,
21
+ }.to_json
22
+ end
23
+
24
+ def metadata
25
+ {
26
+ erb_lint_version: ERBLint::VERSION,
27
+ ruby_engine: RUBY_ENGINE,
28
+ ruby_version: RUBY_VERSION,
29
+ ruby_patchlevel: RUBY_PATCHLEVEL.to_s,
30
+ ruby_platform: RUBY_PLATFORM,
31
+ }
32
+ end
33
+
34
+ def summary
35
+ {
36
+ offenses: stats.found,
37
+ inspected_files: stats.processed_files.size,
38
+ corrected: stats.corrected,
39
+ }
40
+ end
41
+
42
+ def formatted_files
43
+ processed_files.map do |filename, offenses|
44
+ {
45
+ path: filename,
46
+ offenses: formatted_offenses(offenses),
47
+ }
48
+ end
49
+ end
50
+
51
+ def formatted_offenses(offenses)
52
+ offenses.map do |offense|
53
+ format_offense(offense)
54
+ end
55
+ end
56
+
57
+ def format_offense(offense)
58
+ {
59
+ linter: offense.linter.class.simple_name,
60
+ message: offense.message.to_s,
61
+ location: {
62
+ start_line: offense.line_number,
63
+ start_column: offense.column,
64
+ last_line: offense.source_range.last_line,
65
+ last_column: offense.source_range.last_column,
66
+ length: offense.source_range.length,
67
+ },
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ require_relative "compact_reporter"
3
+
4
+ module ERBLint
5
+ module Reporters
6
+ class MultilineReporter < CompactReporter
7
+ private
8
+
9
+ def format_offense(filename, offense)
10
+ <<~EOF
11
+
12
+ #{offense.message}#{Rainbow(' (not autocorrected)').red if autocorrect}
13
+ In file: #{filename}:#{offense.line_number}
14
+ EOF
15
+ end
16
+
17
+ def footer
18
+ puts
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,7 +10,6 @@ module ERBLint
10
10
  @config = config || RunnerConfig.default
11
11
  raise ArgumentError, 'expect `config` to be a RunnerConfig instance' unless @config.is_a?(RunnerConfig)
12
12
 
13
- LinterRegistry.load_custom_linters
14
13
  linter_classes = LinterRegistry.linters.select { |klass| @config.for_linter(klass).enabled? }
15
14
  @linters = linter_classes.map do |linter_class|
16
15
  linter_class.new(@file_loader, @config.for_linter(linter_class))
@@ -61,6 +61,7 @@ module ERBLint
61
61
  SpaceIndentation: { enabled: default_enabled },
62
62
  SpaceInHtmlTag: { enabled: default_enabled },
63
63
  TrailingWhitespace: { enabled: default_enabled },
64
+ RequireInputAutocomplete: { enabled: default_enabled },
64
65
  },
65
66
  )
66
67
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module ERBLint
3
+ class Stats
4
+ attr_accessor :ignored,
5
+ :found,
6
+ :corrected,
7
+ :exceptions,
8
+ :linters,
9
+ :files,
10
+ :processed_files
11
+
12
+ def initialize(
13
+ ignored: 0,
14
+ found: 0,
15
+ corrected: 0,
16
+ exceptions: 0,
17
+ linters: 0,
18
+ files: 0,
19
+ processed_files: {}
20
+ )
21
+ @ignored = ignored
22
+ @found = found
23
+ @corrected = corrected
24
+ @exceptions = exceptions
25
+ @linters = linters
26
+ @files = files
27
+ @processed_files = processed_files
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Utils
5
+ module SeverityLevels
6
+ SEVERITY_NAMES = %i[info refactor convention warning error fatal].freeze
7
+
8
+ SEVERITY_CODE_TABLE = { I: :info, R: :refactor, C: :convention,
9
+ W: :warning, E: :error, F: :fatal }.freeze
10
+
11
+ def severity_level_for_name(name)
12
+ SEVERITY_NAMES.index(name || :error) + 1
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ERBLint
4
- VERSION = '0.0.34'
4
+ VERSION = '0.1.0'
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.34
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Chan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-15 00:00:00.000000000 Z
11
+ date: 2021-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: better_html
@@ -42,16 +42,30 @@ dependencies:
42
42
  name: rubocop
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0.79'
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.7.1.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
53
67
  - !ruby/object:Gem::Version
54
- version: '0.79'
68
+ version: 2.7.1.4
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: activesupport
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-shopify
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  description: ERB Linter tool.
126
154
  email:
127
155
  - justin.the.c@gmail.com
@@ -132,6 +160,7 @@ extra_rdoc_files: []
132
160
  files:
133
161
  - exe/erblint
134
162
  - lib/erb_lint.rb
163
+ - lib/erb_lint/all.rb
135
164
  - lib/erb_lint/cli.rb
136
165
  - lib/erb_lint/corrector.rb
137
166
  - lib/erb_lint/file_loader.rb
@@ -147,6 +176,9 @@ files:
147
176
  - lib/erb_lint/linters/hard_coded_string.rb
148
177
  - lib/erb_lint/linters/no_javascript_tag_helper.rb
149
178
  - lib/erb_lint/linters/parser_errors.rb
179
+ - lib/erb_lint/linters/partial_instance_variable.rb
180
+ - lib/erb_lint/linters/require_input_autocomplete.rb
181
+ - lib/erb_lint/linters/require_script_nonce.rb
150
182
  - lib/erb_lint/linters/right_trim.rb
151
183
  - lib/erb_lint/linters/rubocop.rb
152
184
  - lib/erb_lint/linters/rubocop_text.rb
@@ -157,12 +189,18 @@ files:
157
189
  - lib/erb_lint/linters/trailing_whitespace.rb
158
190
  - lib/erb_lint/offense.rb
159
191
  - lib/erb_lint/processed_source.rb
192
+ - lib/erb_lint/reporter.rb
193
+ - lib/erb_lint/reporters/compact_reporter.rb
194
+ - lib/erb_lint/reporters/json_reporter.rb
195
+ - lib/erb_lint/reporters/multiline_reporter.rb
160
196
  - lib/erb_lint/runner.rb
161
197
  - lib/erb_lint/runner_config.rb
162
198
  - lib/erb_lint/runner_config_resolver.rb
199
+ - lib/erb_lint/stats.rb
163
200
  - lib/erb_lint/utils/block_map.rb
164
201
  - lib/erb_lint/utils/offset_corrector.rb
165
202
  - lib/erb_lint/utils/ruby_to_erb.rb
203
+ - lib/erb_lint/utils/severity_levels.rb
166
204
  - lib/erb_lint/version.rb
167
205
  homepage: https://github.com/Shopify/erb-lint
168
206
  licenses:
@@ -177,14 +215,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
177
215
  requirements:
178
216
  - - ">="
179
217
  - !ruby/object:Gem::Version
180
- version: 2.4.0
218
+ version: 2.5.0
181
219
  required_rubygems_version: !ruby/object:Gem::Requirement
182
220
  requirements:
183
221
  - - ">="
184
222
  - !ruby/object:Gem::Version
185
223
  version: '0'
186
224
  requirements: []
187
- rubygems_version: 3.0.3
225
+ rubygems_version: 3.2.20
188
226
  signing_key:
189
227
  specification_version: 4
190
228
  summary: ERB lint tool