erb_lint 0.0.35 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/erblint +1 -1
- data/lib/erb_lint.rb +1 -17
- data/lib/erb_lint/all.rb +26 -0
- data/lib/erb_lint/cli.rb +101 -54
- data/lib/erb_lint/corrector.rb +1 -1
- data/lib/erb_lint/linter.rb +6 -5
- data/lib/erb_lint/linter_config.rb +3 -3
- data/lib/erb_lint/linter_registry.rb +14 -5
- data/lib/erb_lint/linters/allowed_script_type.rb +7 -7
- data/lib/erb_lint/linters/closing_erb_tag_indent.rb +2 -2
- data/lib/erb_lint/linters/deprecated_classes.rb +7 -7
- data/lib/erb_lint/linters/erb_safety.rb +2 -2
- data/lib/erb_lint/linters/extra_newline.rb +1 -1
- data/lib/erb_lint/linters/final_newline.rb +2 -2
- data/lib/erb_lint/linters/hard_coded_string.rb +36 -16
- data/lib/erb_lint/linters/no_javascript_tag_helper.rb +8 -8
- data/lib/erb_lint/linters/partial_instance_variable.rb +23 -0
- data/lib/erb_lint/linters/require_input_autocomplete.rb +121 -0
- data/lib/erb_lint/linters/require_script_nonce.rb +92 -0
- data/lib/erb_lint/linters/right_trim.rb +1 -1
- data/lib/erb_lint/linters/rubocop.rb +11 -11
- data/lib/erb_lint/linters/rubocop_text.rb +1 -1
- data/lib/erb_lint/linters/self_closing_tag.rb +5 -7
- data/lib/erb_lint/linters/space_around_erb_tag.rb +5 -5
- data/lib/erb_lint/linters/space_in_html_tag.rb +6 -6
- data/lib/erb_lint/linters/space_indentation.rb +1 -1
- data/lib/erb_lint/linters/trailing_whitespace.rb +1 -1
- data/lib/erb_lint/offense.rb +15 -4
- data/lib/erb_lint/reporter.rb +39 -0
- data/lib/erb_lint/reporters/compact_reporter.rb +66 -0
- data/lib/erb_lint/reporters/json_reporter.rb +72 -0
- data/lib/erb_lint/reporters/multiline_reporter.rb +22 -0
- data/lib/erb_lint/runner.rb +1 -2
- data/lib/erb_lint/runner_config.rb +8 -7
- data/lib/erb_lint/runner_config_resolver.rb +4 -4
- data/lib/erb_lint/stats.rb +30 -0
- data/lib/erb_lint/utils/block_map.rb +2 -2
- data/lib/erb_lint/utils/offset_corrector.rb +1 -1
- data/lib/erb_lint/utils/ruby_to_erb.rb +5 -5
- data/lib/erb_lint/utils/severity_levels.rb +16 -0
- data/lib/erb_lint/version.rb +1 -1
- metadata +17 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b98507494f8af2a33e0e6fcbf28c48a2d91c78f42273a3cc6c1206578f3a55b0
|
4
|
+
data.tar.gz: c090a633a9d041a6d4d71bc292b1525b3a74dccd61311e0224bc1cbb0d8fea33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2ac77d88b7be36010d69c072e3ee72c370edefa1fc3b68135593b5b494da8b0f9eac79e9b5408d10155a007d94948ae6a4b3b279de2541be66f0576abc964a8
|
7
|
+
data.tar.gz: f0be6a75c5060b49a8b7c5e5a5d19adb3222fb580a11c2e2e488e5b83ada00db0c4daee25e99ba5538057c749f5937b80d25a96bda1ed967e6d1baeddf787965
|
data/exe/erblint
CHANGED
data/lib/erb_lint.rb
CHANGED
@@ -1,19 +1,3 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
require 'erb_lint/corrector'
|
6
|
-
require 'erb_lint/file_loader'
|
7
|
-
require 'erb_lint/linter_config'
|
8
|
-
require 'erb_lint/linter_registry'
|
9
|
-
require 'erb_lint/linter'
|
10
|
-
require 'erb_lint/offense'
|
11
|
-
require 'erb_lint/processed_source'
|
12
|
-
require 'erb_lint/runner_config'
|
13
|
-
require 'erb_lint/runner'
|
14
|
-
require 'erb_lint/version'
|
15
|
-
|
16
|
-
# Load linters
|
17
|
-
Dir[File.expand_path('erb_lint/linters/**/*.rb', File.dirname(__FILE__))].each do |file|
|
18
|
-
require file
|
19
|
-
end
|
3
|
+
require "erb_lint/version"
|
data/lib/erb_lint/all.rb
ADDED
@@ -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
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
3
|
+
require "erb_lint/all"
|
4
|
+
require "active_support"
|
5
|
+
require "active_support/inflector"
|
6
|
+
require "optparse"
|
7
|
+
require "psych"
|
8
|
+
require "yaml"
|
9
|
+
require "rainbow"
|
10
|
+
require "erb_lint/utils/severity_levels"
|
10
11
|
|
11
12
|
module ERBLint
|
12
13
|
class CLI
|
13
|
-
|
14
|
+
include Utils::SeverityLevels
|
15
|
+
|
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
|
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
|
|
@@ -48,22 +43,27 @@ module ERBLint
|
|
48
43
|
ensure_files_exist(lint_files)
|
49
44
|
|
50
45
|
if enabled_linter_classes.empty?
|
51
|
-
failure!(
|
46
|
+
failure!("no linter available with current configuration")
|
52
47
|
end
|
53
48
|
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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 =
|
102
|
+
file_content = read_content(filename)
|
110
103
|
|
111
104
|
7.times do
|
112
105
|
processed_source = ERBLint::ProcessedSource.new(filename, file_content)
|
@@ -119,27 +112,40 @@ module ERBLint
|
|
119
112
|
|
120
113
|
@stats.corrected += corrector.corrections.size
|
121
114
|
|
122
|
-
|
123
|
-
|
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
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
137
|
-
|
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)
|
141
147
|
corrector = ERBLint::Corrector.new(processed_source, offenses)
|
142
|
-
failure!(corrector.diagnostics.join(
|
148
|
+
failure!(corrector.diagnostics.join(", ")) if corrector.diagnostics.any?
|
143
149
|
corrector
|
144
150
|
end
|
145
151
|
|
@@ -177,7 +183,7 @@ module ERBLint
|
|
177
183
|
else
|
178
184
|
@files
|
179
185
|
.map { |f| Dir.exist?(f) ? Dir[File.join(f, glob)] : f }
|
180
|
-
.map { |f| f.include?(
|
186
|
+
.map { |f| f.include?("*") ? Dir[f] : f }
|
181
187
|
.flatten
|
182
188
|
.map { |f| File.expand_path(f, Dir.pwd) }
|
183
189
|
.select { |filename| !excluded?(filename) }
|
@@ -190,7 +196,8 @@ module ERBLint
|
|
190
196
|
|
191
197
|
def excluded?(filename)
|
192
198
|
@config.global_exclude.any? do |path|
|
193
|
-
File.
|
199
|
+
expanded_path = File.expand_path(path, Dir.pwd)
|
200
|
+
File.fnmatch?(expanded_path, filename)
|
194
201
|
end
|
195
202
|
end
|
196
203
|
|
@@ -233,14 +240,14 @@ module ERBLint
|
|
233
240
|
end
|
234
241
|
|
235
242
|
def relative_filename(filename)
|
236
|
-
filename.sub("#{File.expand_path(
|
243
|
+
filename.sub("#{File.expand_path(".", Dir.pwd)}/", "")
|
237
244
|
end
|
238
245
|
|
239
246
|
def runner_config_override
|
240
247
|
RunnerConfig.new(
|
241
248
|
linters: {}.tap do |linters|
|
242
249
|
ERBLint::LinterRegistry.linters.map do |klass|
|
243
|
-
linters[klass.simple_name] = {
|
250
|
+
linters[klass.simple_name] = { "enabled" => enabled_linter_classes.include?(klass) }
|
244
251
|
end
|
245
252
|
end
|
246
253
|
)
|
@@ -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
|
@@ -267,19 +283,36 @@ module ERBLint
|
|
267
283
|
end
|
268
284
|
|
269
285
|
opts.on("--enable-linters LINTER[,LINTER,...]", Array,
|
270
|
-
"Only use specified linter", "Known linters are: #{known_linter_names.join(
|
286
|
+
"Only use specified linter", "Known linters are: #{known_linter_names.join(", ")}") do |linters|
|
271
287
|
linters.each do |linter|
|
272
288
|
unless known_linter_names.include?(linter)
|
273
|
-
failure!("#{linter}: not a valid linter name (#{known_linter_names.join(
|
289
|
+
failure!("#{linter}: not a valid linter name (#{known_linter_names.join(", ")})")
|
274
290
|
end
|
275
291
|
end
|
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
|
data/lib/erb_lint/corrector.rb
CHANGED
data/lib/erb_lint/linter.rb
CHANGED
@@ -14,9 +14,10 @@ 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
|
-
|
18
|
-
|
19
|
-
name_parts
|
17
|
+
super
|
18
|
+
linter.simple_name = if linter.name.start_with?("ERBLint::Linters::")
|
19
|
+
name_parts = linter.name.split("::")
|
20
|
+
name_parts[2..-1].join("::")
|
20
21
|
else
|
21
22
|
linter.name
|
22
23
|
end
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "active_support"
|
4
|
+
require "smart_properties"
|
5
5
|
|
6
6
|
module ERBLint
|
7
7
|
class LinterConfig
|
@@ -27,7 +27,7 @@ module ERBLint
|
|
27
27
|
allowed_keys = self.class.properties.keys.map(&:to_s)
|
28
28
|
given_keys = config.keys
|
29
29
|
if (extra_keys = given_keys - allowed_keys).any?
|
30
|
-
raise Error, "Given key is not allowed: #{extra_keys.join(
|
30
|
+
raise Error, "Given key is not allowed: #{extra_keys.join(", ")}"
|
31
31
|
end
|
32
32
|
super(config)
|
33
33
|
rescue SmartProperties::InitializationError => e
|
@@ -3,22 +3,31 @@
|
|
3
3
|
module ERBLint
|
4
4
|
# Stores all linters available to the application.
|
5
5
|
module LinterRegistry
|
6
|
-
CUSTOM_LINTERS_DIR =
|
7
|
-
@
|
6
|
+
CUSTOM_LINTERS_DIR = ".erb-linters"
|
7
|
+
@loaded_linters = []
|
8
8
|
|
9
9
|
class << self
|
10
|
-
|
10
|
+
def clear
|
11
|
+
@linters = nil
|
12
|
+
end
|
11
13
|
|
12
14
|
def included(linter_class)
|
13
|
-
@
|
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
|
-
ruby_files = Dir.glob(File.expand_path(File.join(directory,
|
30
|
+
ruby_files = Dir.glob(File.expand_path(File.join(directory, "**", "*.rb")))
|
22
31
|
ruby_files.each { |file| require file }
|
23
32
|
end
|
24
33
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "better_html"
|
4
|
+
require "better_html/tree/tag"
|
5
5
|
|
6
6
|
module ERBLint
|
7
7
|
module Linters
|
@@ -13,7 +13,7 @@ module ERBLint
|
|
13
13
|
|
14
14
|
class ConfigSchema < LinterConfig
|
15
15
|
property :allowed_types, accepts: array_of?(String),
|
16
|
-
default: -> { [
|
16
|
+
default: -> { ["text/javascript"] }
|
17
17
|
property :allow_blank, accepts: [true, false], default: true, reader: :allow_blank?
|
18
18
|
property :disallow_inline_scripts, accepts: [true, false], default: false, reader: :disallow_inline_scripts?
|
19
19
|
end
|
@@ -24,7 +24,7 @@ module ERBLint
|
|
24
24
|
parser.nodes_with_type(:tag).each do |tag_node|
|
25
25
|
tag = BetterHtml::Tree::Tag.from_node(tag_node)
|
26
26
|
next if tag.closing?
|
27
|
-
next unless tag.name ==
|
27
|
+
next unless tag.name == "script"
|
28
28
|
|
29
29
|
if @config.disallow_inline_scripts?
|
30
30
|
name_node = tag_node.to_a[1]
|
@@ -36,7 +36,7 @@ module ERBLint
|
|
36
36
|
next
|
37
37
|
end
|
38
38
|
|
39
|
-
type_attribute = tag.attributes[
|
39
|
+
type_attribute = tag.attributes["type"]
|
40
40
|
type_present = type_attribute.present? && type_attribute.value_node.present?
|
41
41
|
|
42
42
|
if !type_present && !@config.allow_blank?
|
@@ -50,8 +50,8 @@ module ERBLint
|
|
50
50
|
add_offense(
|
51
51
|
type_attribute.loc,
|
52
52
|
"Avoid using #{type_attribute.value.inspect} as type for `<script>` tag. "\
|
53
|
-
"Must be one of: #{@config.allowed_types.join(
|
54
|
-
"#{
|
53
|
+
"Must be one of: #{@config.allowed_types.join(", ")}"\
|
54
|
+
"#{" (or no type attribute)" if @config.allow_blank?}."
|
55
55
|
)
|
56
56
|
end
|
57
57
|
end
|