theme-check 0.1.0 → 0.3.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 +4 -4
- data/.gitignore +1 -0
- data/CONTRIBUTING.md +2 -0
- data/README.md +62 -1
- data/RELEASING.md +41 -0
- data/Rakefile +38 -4
- data/config/default.yml +18 -0
- data/data/shopify_liquid/deprecated_filters.yml +10 -0
- data/data/shopify_liquid/plus_objects.yml +15 -0
- data/dev.yml +2 -0
- data/lib/theme_check.rb +5 -0
- data/lib/theme_check/analyzer.rb +15 -5
- data/lib/theme_check/check.rb +11 -0
- data/lib/theme_check/checks.rb +10 -0
- data/lib/theme_check/checks/deprecated_filter.rb +22 -0
- data/lib/theme_check/checks/missing_enable_comment.rb +31 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +12 -12
- data/lib/theme_check/checks/parser_blocking_javascript.rb +55 -0
- data/lib/theme_check/checks/space_inside_braces.rb +19 -7
- data/lib/theme_check/checks/template_length.rb +11 -3
- data/lib/theme_check/checks/undefined_object.rb +107 -34
- data/lib/theme_check/checks/valid_html_translation.rb +2 -2
- data/lib/theme_check/cli.rb +18 -4
- data/lib/theme_check/config.rb +97 -44
- data/lib/theme_check/corrector.rb +31 -0
- data/lib/theme_check/disabled_checks.rb +77 -0
- data/lib/theme_check/file_system_storage.rb +51 -0
- data/lib/theme_check/in_memory_storage.rb +37 -0
- data/lib/theme_check/json_file.rb +12 -10
- data/lib/theme_check/language_server/handler.rb +38 -13
- data/lib/theme_check/language_server/server.rb +2 -2
- data/lib/theme_check/liquid_check.rb +2 -2
- data/lib/theme_check/node.rb +13 -0
- data/lib/theme_check/offense.rb +25 -9
- data/lib/theme_check/packager.rb +51 -0
- data/lib/theme_check/printer.rb +13 -4
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +28 -0
- data/lib/theme_check/shopify_liquid/object.rb +6 -0
- data/lib/theme_check/storage.rb +25 -0
- data/lib/theme_check/template.rb +32 -10
- data/lib/theme_check/theme.rb +14 -9
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +14 -3
- data/packaging/homebrew/theme_check.base.rb +98 -0
- metadata +21 -7
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when trying to use parser-blocking script tags
|
4
|
+
class ParserBlockingJavaScript < LiquidCheck
|
5
|
+
severity :error
|
6
|
+
category :liquid
|
7
|
+
|
8
|
+
PARSER_BLOCKING_SCRIPT_TAG = %r{
|
9
|
+
<script # Find the start of a script tag
|
10
|
+
(?=(?:[^>]|\n|\r)+?src=)+? # Make sure src= is in the script with a lookahead
|
11
|
+
(?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
|
12
|
+
>
|
13
|
+
}xim
|
14
|
+
SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
|
15
|
+
|
16
|
+
def on_document(node)
|
17
|
+
@source = node.template.source
|
18
|
+
@node = node
|
19
|
+
record_offenses
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def record_offenses
|
25
|
+
record_offenses_from_regex(
|
26
|
+
message: "Missing async or defer attribute on script tag",
|
27
|
+
regex: PARSER_BLOCKING_SCRIPT_TAG,
|
28
|
+
)
|
29
|
+
record_offenses_from_regex(
|
30
|
+
message: "The script_tag filter is parser-blocking. Use a script tag with the async or defer attribute for better performance",
|
31
|
+
regex: SCRIPT_TAG_FILTER,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The trickiness here is matching on scripts that are defined on
|
36
|
+
# multiple lines (or repeat matches). This makes the line_number
|
37
|
+
# calculation a bit weird. So instead, we traverse the string in
|
38
|
+
# a very imperative way.
|
39
|
+
def record_offenses_from_regex(regex: nil, message: nil)
|
40
|
+
i = 0
|
41
|
+
while (i = @source.index(regex, i))
|
42
|
+
script = @source.match(regex, i)[0]
|
43
|
+
|
44
|
+
add_offense(
|
45
|
+
message,
|
46
|
+
node: @node,
|
47
|
+
markup: script,
|
48
|
+
line_number: @source[0...i].count("\n") + 1
|
49
|
+
)
|
50
|
+
|
51
|
+
i += script.size
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -11,6 +11,7 @@ module ThemeCheck
|
|
11
11
|
|
12
12
|
def on_node(node)
|
13
13
|
return unless node.markup
|
14
|
+
return if :assign == node.type_name
|
14
15
|
|
15
16
|
outside_of_strings(node.markup) do |chunk|
|
16
17
|
chunk.scan(/([,:]) +/) do |_match|
|
@@ -45,13 +46,24 @@ module ThemeCheck
|
|
45
46
|
def on_variable(node)
|
46
47
|
return if @ignore
|
47
48
|
if node.markup[0] != " "
|
48
|
-
add_offense("Space missing after '{{'", node: node)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
add_offense("Space missing after '{{'", node: node) do |corrector|
|
50
|
+
corrector.insert_before(node, " ")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if node.markup[-1] != " "
|
54
|
+
add_offense("Space missing before '}}'", node: node) do |corrector|
|
55
|
+
corrector.insert_after(node, " ")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if node.markup[0] == " " && node.markup[1] == " "
|
59
|
+
add_offense("Too many spaces after '{{'", node: node) do |corrector|
|
60
|
+
corrector.replace(node, " #{node.markup.lstrip}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
if node.markup[-1] == " " && node.markup[-2] == " "
|
64
|
+
add_offense("Too many spaces before '}}'", node: node) do |corrector|
|
65
|
+
corrector.replace(node, "#{node.markup.rstrip} ")
|
66
|
+
end
|
55
67
|
end
|
56
68
|
end
|
57
69
|
end
|
@@ -4,12 +4,20 @@ module ThemeCheck
|
|
4
4
|
severity :suggestion
|
5
5
|
category :liquid
|
6
6
|
|
7
|
-
def initialize(max_length: 200)
|
7
|
+
def initialize(max_length: 200, exclude_schema: true)
|
8
8
|
@max_length = max_length
|
9
|
+
@exclude_schema = exclude_schema
|
10
|
+
@excluded_lines = 0
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
+
def on_schema(node)
|
14
|
+
if @exclude_schema
|
15
|
+
@excluded_lines += node.value.nodelist.join.count("\n")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_document(node)
|
20
|
+
lines = node.template.source.count("\n") - @excluded_lines
|
13
21
|
if lines > @max_length
|
14
22
|
add_offense("Template has too many lines [#{lines}/#{@max_length}]", template: node.template)
|
15
23
|
end
|
@@ -11,76 +11,149 @@ module ThemeCheck
|
|
11
11
|
@all_assigns = {}
|
12
12
|
@all_captures = {}
|
13
13
|
@all_forloops = {}
|
14
|
+
@all_renders = {}
|
14
15
|
end
|
15
16
|
|
16
|
-
attr_reader :
|
17
|
-
|
17
|
+
attr_reader :all_assigns, :all_captures, :all_forloops
|
18
|
+
|
19
|
+
def add_render(name:, node:)
|
20
|
+
@all_renders[name] = node
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_variable_lookup(name:, node:)
|
24
|
+
line_number = node.parent.line_number
|
25
|
+
key = [name, line_number]
|
26
|
+
@all_variable_lookups[key] = node
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_variables
|
18
30
|
all_assigns.keys + all_captures.keys + all_forloops.keys
|
19
31
|
end
|
32
|
+
|
33
|
+
def each_snippet
|
34
|
+
@all_renders.each do |(name, info)|
|
35
|
+
yield [name, info]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def each_variable_lookup(unique_keys = false)
|
40
|
+
seen = Set.new
|
41
|
+
@all_variable_lookups.each do |(key, info)|
|
42
|
+
name, _line_number = key
|
43
|
+
|
44
|
+
next if unique_keys && seen.include?(name)
|
45
|
+
seen << name
|
46
|
+
|
47
|
+
yield [key, info]
|
48
|
+
end
|
49
|
+
end
|
20
50
|
end
|
21
51
|
|
22
52
|
def initialize
|
23
|
-
@
|
24
|
-
@used_snippets = {}
|
53
|
+
@files = {}
|
25
54
|
end
|
26
55
|
|
27
56
|
def on_document(node)
|
28
|
-
@
|
57
|
+
@files[node.template.name] = TemplateInfo.new
|
29
58
|
end
|
30
59
|
|
31
60
|
def on_assign(node)
|
32
|
-
@
|
61
|
+
@files[node.template.name].all_assigns[node.value.to] = node
|
33
62
|
end
|
34
63
|
|
35
64
|
def on_capture(node)
|
36
|
-
@
|
65
|
+
@files[node.template.name].all_captures[node.value.instance_variable_get('@to')] = node
|
37
66
|
end
|
38
67
|
|
39
68
|
def on_for(node)
|
40
|
-
@
|
69
|
+
@files[node.template.name].all_forloops[node.value.variable_name] = node
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_include(_node)
|
73
|
+
# NOOP: we purposely do nothing on `include` since it is deprecated
|
74
|
+
# https://shopify.dev/docs/themes/liquid/reference/tags/deprecated-tags#include
|
41
75
|
end
|
42
76
|
|
43
|
-
def
|
77
|
+
def on_render(node)
|
44
78
|
return unless node.value.template_name_expr.is_a?(String)
|
45
|
-
|
46
|
-
|
47
|
-
@
|
79
|
+
|
80
|
+
snippet_name = "snippets/#{node.value.template_name_expr}"
|
81
|
+
@files[node.template.name].add_render(
|
82
|
+
name: snippet_name,
|
83
|
+
node: node,
|
84
|
+
)
|
48
85
|
end
|
49
86
|
|
50
87
|
def on_variable_lookup(node)
|
51
|
-
@
|
88
|
+
@files[node.template.name].add_variable_lookup(
|
89
|
+
name: node.value.name,
|
90
|
+
node: node,
|
91
|
+
)
|
52
92
|
end
|
53
93
|
|
54
94
|
def on_end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
95
|
+
all_global_objects = ThemeCheck::ShopifyLiquid::Object.labels
|
96
|
+
all_global_objects.freeze
|
97
|
+
|
98
|
+
shopify_plus_objects = ThemeCheck::ShopifyLiquid::Object.plus_labels
|
99
|
+
shopify_plus_objects.freeze
|
100
|
+
|
101
|
+
each_template do |(name, info)|
|
102
|
+
if 'templates/customers/reset_password' == name
|
103
|
+
# NOTE: `email` is exceptionally exposed as a theme object in
|
104
|
+
# the customers' reset password template
|
105
|
+
check_object(info, all_global_objects + ['email'])
|
106
|
+
elsif 'layout/checkout' == name
|
107
|
+
# NOTE: Shopify Plus has exceptionally exposed objects in
|
108
|
+
# the checkout template
|
109
|
+
# https://shopify.dev/docs/themes/theme-templates/checkout-liquid#optional-objects
|
110
|
+
check_object(info, all_global_objects + shopify_plus_objects)
|
66
111
|
else
|
67
|
-
|
68
|
-
all += ['email'] if 'templates/customers/reset_password' == template_name
|
69
|
-
check_object(info.all_variable_lookups, all)
|
112
|
+
check_object(info, all_global_objects)
|
70
113
|
end
|
71
114
|
end
|
72
115
|
end
|
73
116
|
|
74
|
-
def
|
75
|
-
|
76
|
-
next if
|
77
|
-
|
117
|
+
def each_template
|
118
|
+
@files.each do |(name, info)|
|
119
|
+
next if name.starts_with?('snippets/')
|
120
|
+
yield [name, info]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
private :each_template
|
124
|
+
|
125
|
+
def check_object(info, all_global_objects, render_node = nil)
|
126
|
+
check_undefined(info, all_global_objects, render_node)
|
78
127
|
|
79
|
-
|
80
|
-
|
81
|
-
|
128
|
+
info.each_snippet do |(snippet_name, node)|
|
129
|
+
snippet_info = @files[snippet_name]
|
130
|
+
next unless snippet_info # NOTE: undefined snippet
|
131
|
+
|
132
|
+
snippet_variables = node.value.attributes.keys +
|
133
|
+
Array[node.value.instance_variable_get("@alias_name")]
|
134
|
+
check_object(snippet_info, all_global_objects + snippet_variables, node)
|
82
135
|
end
|
83
136
|
end
|
84
137
|
private :check_object
|
138
|
+
|
139
|
+
def check_undefined(info, all_global_objects, render_node)
|
140
|
+
all_variables = info.all_variables
|
141
|
+
|
142
|
+
info.each_variable_lookup(!!render_node) do |(key, node)|
|
143
|
+
name, _line_number = key
|
144
|
+
next if all_variables.include?(name)
|
145
|
+
next if all_global_objects.include?(name)
|
146
|
+
|
147
|
+
node = node.parent
|
148
|
+
node = node.parent if %i(condition variable_lookup).include?(node.type_name)
|
149
|
+
|
150
|
+
if render_node
|
151
|
+
add_offense("Missing argument `#{name}`", node: render_node)
|
152
|
+
else
|
153
|
+
add_offense("Undefined object `#{name}`", node: node)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
private :check_undefined
|
85
158
|
end
|
86
159
|
end
|
@@ -20,7 +20,7 @@ module ThemeCheck
|
|
20
20
|
keys[-1].end_with?('_html') || pluralized_key.end_with?('_html')
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def parse_and_add_offense(key, value)
|
24
24
|
return unless value.is_a?(String)
|
25
25
|
|
26
26
|
html = Nokogiri::HTML5.fragment(value, max_errors: -1)
|
@@ -36,7 +36,7 @@ module ThemeCheck
|
|
36
36
|
visit_nested(v, keys + [k])
|
37
37
|
end
|
38
38
|
elsif html_key?(keys)
|
39
|
-
|
39
|
+
parse_and_add_offense(keys.join('.'), value)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
data/lib/theme_check/cli.rb
CHANGED
@@ -10,7 +10,9 @@ module ThemeCheck
|
|
10
10
|
-c, [--category] # Only run this category of checks
|
11
11
|
-x, [--exclude-category] # Exclude this category of checks
|
12
12
|
-l, [--list] # List enabled checks
|
13
|
+
-a, [--auto-correct] # Automatically fix offenses
|
13
14
|
-h, [--help] # Show this. Hi!
|
15
|
+
-v, [--version] # Print Theme Check version
|
14
16
|
|
15
17
|
Description:
|
16
18
|
Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
|
@@ -25,18 +27,23 @@ module ThemeCheck
|
|
25
27
|
command = :check
|
26
28
|
only_categories = []
|
27
29
|
exclude_categories = []
|
30
|
+
auto_correct = false
|
28
31
|
|
29
32
|
args = argv.dup
|
30
33
|
while (arg = args.shift)
|
31
34
|
case arg
|
32
35
|
when "--help", "-h"
|
33
36
|
raise Abort, USAGE
|
37
|
+
when "--version", "-v"
|
38
|
+
command = :version
|
34
39
|
when "--category", "-c"
|
35
40
|
only_categories << args.shift.to_sym
|
36
41
|
when "--exclude-category", "-x"
|
37
42
|
exclude_categories << args.shift.to_sym
|
38
43
|
when "--list", "-l"
|
39
44
|
command = :list
|
45
|
+
when "--auto-correct", "-a"
|
46
|
+
auto_correct = true
|
40
47
|
else
|
41
48
|
path = arg
|
42
49
|
end
|
@@ -45,6 +52,7 @@ module ThemeCheck
|
|
45
52
|
@config = ThemeCheck::Config.from_path(path)
|
46
53
|
@config.only_categories = only_categories
|
47
54
|
@config.exclude_categories = exclude_categories
|
55
|
+
@config.auto_correct = auto_correct
|
48
56
|
|
49
57
|
send(command)
|
50
58
|
end
|
@@ -63,16 +71,22 @@ module ThemeCheck
|
|
63
71
|
puts @config.enabled_checks
|
64
72
|
end
|
65
73
|
|
74
|
+
def version
|
75
|
+
puts ThemeCheck::VERSION
|
76
|
+
end
|
77
|
+
|
66
78
|
def check
|
67
79
|
puts "Checking #{@config.root} ..."
|
68
|
-
|
80
|
+
storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
|
81
|
+
theme = ThemeCheck::Theme.new(storage)
|
69
82
|
if theme.all.empty?
|
70
83
|
raise Abort, "No templates found.\n#{USAGE}"
|
71
84
|
end
|
72
|
-
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks)
|
85
|
+
analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
|
73
86
|
analyzer.analyze_theme
|
74
|
-
|
75
|
-
|
87
|
+
analyzer.correct_offenses
|
88
|
+
ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
|
89
|
+
raise Abort, "" if analyzer.uncorrectable_offenses.any?
|
76
90
|
end
|
77
91
|
end
|
78
92
|
end
|
data/lib/theme_check/config.rb
CHANGED
@@ -4,20 +4,29 @@ module ThemeCheck
|
|
4
4
|
class Config
|
5
5
|
DOTFILE = '.theme-check.yml'
|
6
6
|
DEFAULT_CONFIG = "#{__dir__}/../../config/default.yml"
|
7
|
+
BOOLEAN = [true, false]
|
7
8
|
|
8
9
|
attr_reader :root
|
9
|
-
attr_accessor :only_categories, :exclude_categories
|
10
|
+
attr_accessor :only_categories, :exclude_categories, :auto_correct
|
10
11
|
|
11
12
|
class << self
|
12
13
|
def from_path(path)
|
13
14
|
if (filename = find(path))
|
14
|
-
new(filename.dirname, load_file(filename))
|
15
|
+
new(root: filename.dirname, configuration: load_file(filename))
|
15
16
|
else
|
16
17
|
# No configuration file
|
17
|
-
new(path)
|
18
|
+
new(root: path)
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
22
|
+
def from_string(config)
|
23
|
+
new(configuration: YAML.load(config), should_resolve_requires: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
def from_hash(config)
|
27
|
+
new(configuration: config, should_resolve_requires: false)
|
28
|
+
end
|
29
|
+
|
21
30
|
def find(root, needle = DOTFILE)
|
22
31
|
Pathname.new(root).descend.reverse_each do |path|
|
23
32
|
pathname = path.join(needle)
|
@@ -29,80 +38,124 @@ module ThemeCheck
|
|
29
38
|
def load_file(absolute_path)
|
30
39
|
YAML.load_file(absolute_path)
|
31
40
|
end
|
41
|
+
|
42
|
+
def default
|
43
|
+
@default ||= load_file(DEFAULT_CONFIG)
|
44
|
+
end
|
32
45
|
end
|
33
46
|
|
34
|
-
def initialize(root, configuration
|
35
|
-
@configuration = configuration
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
47
|
+
def initialize(root: nil, configuration: nil, should_resolve_requires: true)
|
48
|
+
@configuration = if configuration
|
49
|
+
validate_configuration(configuration)
|
50
|
+
else
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
merge_with_default_configuration!(@configuration)
|
54
|
+
|
55
|
+
@root = if root && @configuration.key?("root")
|
56
|
+
Pathname.new(root).join(@configuration["root"])
|
57
|
+
elsif root
|
58
|
+
Pathname.new(root)
|
40
59
|
end
|
60
|
+
|
41
61
|
@only_categories = []
|
42
62
|
@exclude_categories = []
|
43
|
-
|
63
|
+
@auto_correct = false
|
64
|
+
|
65
|
+
resolve_requires if @root && should_resolve_requires
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](name)
|
69
|
+
@configuration[name]
|
44
70
|
end
|
45
71
|
|
46
72
|
def to_h
|
47
73
|
@configuration
|
48
74
|
end
|
49
75
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
default_configuration.merge(@checks).each do |check_name, properties|
|
54
|
-
if @checks[check_name] && !default_configuration[check_name].nil?
|
55
|
-
valid_properties = valid_check_configuration(check_name)
|
56
|
-
properties = properties.merge(valid_properties)
|
57
|
-
end
|
76
|
+
def check_configurations
|
77
|
+
@check_configurations ||= @configuration.select { |name, _| check_name?(name) }
|
78
|
+
end
|
58
79
|
|
59
|
-
|
80
|
+
def enabled_checks
|
81
|
+
@enabled_checks ||= check_configurations.map do |check_name, options|
|
82
|
+
next unless options["enabled"]
|
60
83
|
|
61
|
-
options = properties.transform_keys(&:to_sym)
|
62
84
|
check_class = ThemeCheck.const_get(check_name)
|
85
|
+
|
63
86
|
next if exclude_categories.include?(check_class.category)
|
64
87
|
next if only_categories.any? && !only_categories.include?(check_class.category)
|
65
88
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
89
|
+
options_for_check = options.transform_keys(&:to_sym)
|
90
|
+
options_for_check.delete(:enabled)
|
91
|
+
check = check_class.new(**options_for_check)
|
92
|
+
check.options = options_for_check
|
93
|
+
check
|
94
|
+
end.compact
|
95
|
+
end
|
70
96
|
|
71
|
-
|
97
|
+
def ignored_patterns
|
98
|
+
self["ignore"] || []
|
72
99
|
end
|
73
100
|
|
74
101
|
private
|
75
102
|
|
76
|
-
def
|
77
|
-
|
103
|
+
def check_name?(name)
|
104
|
+
name.start_with?(/[A-Z]/)
|
78
105
|
end
|
79
106
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
107
|
+
def validate_configuration(configuration, default_configuration = self.class.default, parent_keys = [])
|
108
|
+
valid_configuration = {}
|
109
|
+
|
110
|
+
configuration.each do |key, value|
|
111
|
+
# No validation possible unless we have a default to compare to
|
112
|
+
unless default_configuration
|
113
|
+
valid_configuration[key] = value
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
default = default_configuration[key]
|
118
|
+
keys = parent_keys + [key]
|
119
|
+
name = keys.join(".")
|
120
|
+
|
121
|
+
if check_name?(key)
|
122
|
+
if value.is_a?(Hash)
|
123
|
+
valid_configuration[key] = validate_configuration(value, default, keys)
|
124
|
+
else
|
125
|
+
warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
|
87
126
|
end
|
127
|
+
elsif default.nil?
|
128
|
+
warn("unknown configuration: #{name}")
|
129
|
+
elsif BOOLEAN.include?(default) && !BOOLEAN.include?(value)
|
130
|
+
warn("bad configuration type for #{name}: expected true or false, got #{value.inspect}")
|
131
|
+
elsif !BOOLEAN.include?(default) && default.class != value.class
|
132
|
+
warn("bad configuration type for #{name}: expected a #{default.class}, got #{value.inspect}")
|
133
|
+
else
|
134
|
+
valid_configuration[key] = value
|
88
135
|
end
|
89
136
|
end
|
90
|
-
end
|
91
137
|
|
92
|
-
|
93
|
-
|
138
|
+
valid_configuration
|
139
|
+
end
|
94
140
|
|
95
|
-
|
141
|
+
def merge_with_default_configuration!(configuration, default_configuration = self.class.default)
|
142
|
+
default_configuration.each do |key, default|
|
143
|
+
value = configuration[key]
|
96
144
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
145
|
+
case value
|
146
|
+
when Hash
|
147
|
+
merge_with_default_configuration!(value, default)
|
148
|
+
when nil
|
149
|
+
configuration[key] = default
|
102
150
|
end
|
103
151
|
end
|
152
|
+
configuration
|
153
|
+
end
|
104
154
|
|
105
|
-
|
155
|
+
def resolve_requires
|
156
|
+
self["require"]&.each do |path|
|
157
|
+
require(File.join(@root, path))
|
158
|
+
end
|
106
159
|
end
|
107
160
|
end
|
108
161
|
end
|