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