erb_lint 0.0.34 → 0.1.0

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
  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