theme-check 1.7.2 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +47 -0
  4. data/README.md +10 -0
  5. data/RELEASING.md +13 -0
  6. data/config/default.yml +5 -0
  7. data/data/shopify_liquid/deprecated_filters.yml +4 -0
  8. data/data/shopify_liquid/filters.yml +3 -1
  9. data/docs/checks/TEMPLATE.md.erb +24 -19
  10. data/docs/checks/schema_json_format.md +76 -0
  11. data/docs/language_server/code-action-command-palette.png +0 -0
  12. data/docs/language_server/code-action-flow.png +0 -0
  13. data/docs/language_server/code-action-keyboard.png +0 -0
  14. data/docs/language_server/code-action-light-bulb.png +0 -0
  15. data/docs/language_server/code-action-problem.png +0 -0
  16. data/docs/language_server/code-action-quickfix.png +0 -0
  17. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  18. data/exe/theme-check-language-server +0 -4
  19. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  20. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  21. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  22. data/lib/theme_check/checks/default_locale.rb +1 -1
  23. data/lib/theme_check/checks/deprecated_filter.rb +81 -4
  24. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  25. data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
  26. data/lib/theme_check/checks/matching_translations.rb +1 -0
  27. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  28. data/lib/theme_check/checks/missing_template.rb +1 -1
  29. data/lib/theme_check/checks/pagination_size.rb +2 -3
  30. data/lib/theme_check/checks/remote_asset.rb +5 -0
  31. data/lib/theme_check/checks/required_directories.rb +1 -1
  32. data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
  33. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  34. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  35. data/lib/theme_check/checks/translation_key_exists.rb +33 -13
  36. data/lib/theme_check/checks/unused_assign.rb +3 -2
  37. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  38. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  39. data/lib/theme_check/checks/valid_schema.rb +2 -2
  40. data/lib/theme_check/corrector.rb +34 -23
  41. data/lib/theme_check/file_system_storage.rb +4 -3
  42. data/lib/theme_check/html_node.rb +122 -6
  43. data/lib/theme_check/html_visitor.rb +1 -32
  44. data/lib/theme_check/in_memory_storage.rb +9 -0
  45. data/lib/theme_check/json_helpers.rb +14 -0
  46. data/lib/theme_check/language_server/bridge.rb +19 -5
  47. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  48. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  49. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  50. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  51. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  52. data/lib/theme_check/language_server/configuration.rb +69 -0
  53. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  54. data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
  55. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  56. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  57. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  58. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  59. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  60. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  61. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  62. data/lib/theme_check/language_server/handler.rb +83 -29
  63. data/lib/theme_check/language_server/io_messenger.rb +11 -1
  64. data/lib/theme_check/language_server/protocol.rb +4 -0
  65. data/lib/theme_check/language_server/server.rb +29 -11
  66. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  67. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  68. data/lib/theme_check/language_server.rb +23 -5
  69. data/lib/theme_check/liquid_node.rb +255 -12
  70. data/lib/theme_check/locale_diff.rb +39 -8
  71. data/lib/theme_check/node.rb +16 -0
  72. data/lib/theme_check/offense.rb +27 -23
  73. data/lib/theme_check/position.rb +4 -4
  74. data/lib/theme_check/regex_helpers.rb +1 -1
  75. data/lib/theme_check/schema_helper.rb +70 -0
  76. data/lib/theme_check/storage.rb +4 -0
  77. data/lib/theme_check/tags.rb +0 -1
  78. data/lib/theme_check/theme.rb +1 -1
  79. data/lib/theme_check/theme_file.rb +8 -1
  80. data/lib/theme_check/theme_file_rewriter.rb +28 -6
  81. data/lib/theme_check/version.rb +1 -1
  82. data/lib/theme_check.rb +11 -2
  83. metadata +26 -3
  84. data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -45,11 +45,47 @@ module ThemeCheck
45
45
  def markup
46
46
  if tag?
47
47
  tag_markup
48
+ elsif literal?
49
+ value.to_s
48
50
  elsif @value.instance_variable_defined?(:@markup)
49
51
  @value.instance_variable_get(:@markup)
50
52
  end
51
53
  end
52
54
 
55
+ # The original source code of the node. Does contain wrapping braces.
56
+ def outer_markup
57
+ if literal?
58
+ markup
59
+ elsif variable_lookup?
60
+ ''
61
+ elsif variable?
62
+ start_token + markup + end_token
63
+ elsif tag? && block?
64
+ start_index = block_start_start_index
65
+ end_index = block_start_end_index
66
+ end_index += inner_markup.size
67
+ end_index = find_block_delimiter(end_index)&.end(0)
68
+ source[start_index...end_index]
69
+ elsif tag?
70
+ source[block_start_start_index...block_start_end_index]
71
+ else
72
+ inner_markup
73
+ end
74
+ end
75
+
76
+ def inner_markup
77
+ return '' unless block?
78
+ @inner_markup ||= source[block_start_end_index...block_end_start_index]
79
+ end
80
+
81
+ def inner_json
82
+ return nil unless schema?
83
+ @inner_json ||= JSON.parse(inner_markup)
84
+ rescue JSON::ParserError
85
+ # Handled by ValidSchema
86
+ @inner_json = nil
87
+ end
88
+
53
89
  def markup=(markup)
54
90
  if @value.instance_variable_defined?(:@markup)
55
91
  @value.instance_variable_set(:@markup, markup)
@@ -70,10 +106,26 @@ module ThemeCheck
70
106
  position.start_index
71
107
  end
72
108
 
109
+ def start_row
110
+ position.start_row
111
+ end
112
+
113
+ def start_column
114
+ position.start_column
115
+ end
116
+
73
117
  def end_index
74
118
  position.end_index
75
119
  end
76
120
 
121
+ def end_row
122
+ position.end_row
123
+ end
124
+
125
+ def end_column
126
+ position.end_column
127
+ end
128
+
77
129
  # Literals are hard-coded values in the liquid file.
78
130
  def literal?
79
131
  @value.is_a?(String) || @value.is_a?(Integer)
@@ -88,6 +140,14 @@ module ThemeCheck
88
140
  @value.is_a?(Liquid::Variable)
89
141
  end
90
142
 
143
+ def assigned_or_echoed_variable?
144
+ variable? && start_token == ""
145
+ end
146
+
147
+ def variable_lookup?
148
+ @value.is_a?(Liquid::VariableLookup)
149
+ end
150
+
91
151
  # A {% comment %} block node?
92
152
  def comment?
93
153
  @value.is_a?(Liquid::Comment)
@@ -114,6 +174,10 @@ module ThemeCheck
114
174
  block_tag? || block_body? || document?
115
175
  end
116
176
 
177
+ def schema?
178
+ @value.is_a?(ThemeCheck::Tags::Schema)
179
+ end
180
+
117
181
  # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
118
182
  # and `after_<type_name>` check methods.
119
183
  def type_name
@@ -124,6 +188,86 @@ module ThemeCheck
124
188
  theme_file&.source
125
189
  end
126
190
 
191
+ def block_start_markup
192
+ source[block_start_start_index...block_start_end_index]
193
+ end
194
+
195
+ def block_start_start_index
196
+ @block_start_start_index ||= if inside_liquid_tag?
197
+ backtrack_on_whitespace(source, start_index, /[ \t]/)
198
+ elsif tag?
199
+ backtrack_on_whitespace(source, start_index) - start_token.length
200
+ else
201
+ position.start_index - start_token.length
202
+ end
203
+ end
204
+
205
+ def block_start_end_index
206
+ @block_start_end_index ||= position.end_index + end_token.size
207
+ end
208
+
209
+ def block_end_markup
210
+ source[block_end_start_index...block_end_end_index]
211
+ end
212
+
213
+ def block_end_start_index
214
+ return block_start_end_index unless tag? && block?
215
+ @block_end_start_index ||= block_end_match&.begin(0) || block_start_end_index
216
+ end
217
+
218
+ def block_end_end_index
219
+ return block_end_start_index unless tag? && block?
220
+ @block_end_end_index ||= block_end_match&.end(0) || block_start_end_index
221
+ end
222
+
223
+ def outer_markup_start_index
224
+ outer_markup_position.start_index
225
+ end
226
+
227
+ def outer_markup_end_index
228
+ outer_markup_position.end_index
229
+ end
230
+
231
+ def outer_markup_start_row
232
+ outer_markup_position.start_row
233
+ end
234
+
235
+ def outer_markup_start_column
236
+ outer_markup_position.start_column
237
+ end
238
+
239
+ def outer_markup_end_row
240
+ outer_markup_position.end_row
241
+ end
242
+
243
+ def outer_markup_end_column
244
+ outer_markup_position.end_column
245
+ end
246
+
247
+ def inner_markup_start_index
248
+ inner_markup_position.start_index
249
+ end
250
+
251
+ def inner_markup_end_index
252
+ inner_markup_position.end_index
253
+ end
254
+
255
+ def inner_markup_start_row
256
+ inner_markup_position.start_row
257
+ end
258
+
259
+ def inner_markup_start_column
260
+ inner_markup_position.start_column
261
+ end
262
+
263
+ def inner_markup_end_row
264
+ inner_markup_position.end_row
265
+ end
266
+
267
+ def inner_markup_end_column
268
+ inner_markup_position.end_column
269
+ end
270
+
127
271
  WHITESPACE = /\s/
128
272
 
129
273
  # Is this node inside a `{% liquid ... %}` block?
@@ -165,21 +309,37 @@ module ThemeCheck
165
309
  end
166
310
 
167
311
  def start_token
168
- return "" if inside_liquid_tag?
169
- output = ""
170
- output += "{{" if variable?
171
- output += "{%" if tag?
172
- output += "-" if whitespace_trimmed_start?
173
- output
312
+ if inside_liquid_tag?
313
+ ""
314
+ elsif variable? && source[start_index - 3..start_index - 1] == "{{-"
315
+ "{{-"
316
+ elsif variable? && source[start_index - 2..start_index - 1] == "{{"
317
+ "{{"
318
+ elsif tag? && whitespace_trimmed_start?
319
+ "{%-"
320
+ elsif tag?
321
+ "{%"
322
+ else
323
+ ""
324
+ end
174
325
  end
175
326
 
176
327
  def end_token
177
- return "" if inside_liquid_tag?
178
- output = ""
179
- output += "-" if whitespace_trimmed_end?
180
- output += "}}" if variable?
181
- output += "%}" if tag?
182
- output
328
+ if inside_liquid_tag? && source[end_index] == "\n"
329
+ "\n"
330
+ elsif inside_liquid_tag?
331
+ ""
332
+ elsif variable? && source[end_index...end_index + 3] == "-}}"
333
+ "-}}"
334
+ elsif variable? && source[end_index...end_index + 2] == "}}"
335
+ "}}"
336
+ elsif tag? && whitespace_trimmed_end?
337
+ "-%}"
338
+ elsif tag?
339
+ "%}"
340
+ else # this could happen because we're in an assign statement (variable)
341
+ ""
342
+ end
183
343
  end
184
344
 
185
345
  private
@@ -192,6 +352,22 @@ module ThemeCheck
192
352
  )
193
353
  end
194
354
 
355
+ def outer_markup_position
356
+ @outer_markup_position ||= StrictPosition.new(
357
+ outer_markup,
358
+ source,
359
+ block_start_start_index,
360
+ )
361
+ end
362
+
363
+ def inner_markup_position
364
+ @inner_markup_position ||= StrictPosition.new(
365
+ inner_markup,
366
+ source,
367
+ block_start_end_index,
368
+ )
369
+ end
370
+
195
371
  # Here we're hacking around a glorious bug in Liquid that makes it so the
196
372
  # line_number and markup of a tag is wrong if there's whitespace
197
373
  # between the tag_name and the markup of the tag.
@@ -287,5 +463,72 @@ module ThemeCheck
287
463
  # return the real raw content
288
464
  @tag_markup = source[tag_start...markup_end]
289
465
  end
466
+
467
+ # Returns the index of the leftmost consecutive whitespace
468
+ # starting from start going backwards.
469
+ #
470
+ # e.g. backtrack_on_whitespace("01 45", 4) would return 2.
471
+ # e.g. backtrack_on_whitespace("{% render %}", 5) would return 2.
472
+ def backtrack_on_whitespace(string, start, whitespace = WHITESPACE)
473
+ i = start
474
+ i -= 1 while string[i - 1] =~ whitespace && i > 0
475
+ i
476
+ end
477
+
478
+ def find_block_delimiter(start_index)
479
+ return nil unless tag? && block?
480
+
481
+ tag_start, tag_end = if inside_liquid_tag?
482
+ [
483
+ /^\s*#{@value.tag_name}\s*/,
484
+ /^\s*end#{@value.tag_name}\s*/,
485
+ ]
486
+ else
487
+ [
488
+ /#{Liquid::TagStart}-?\s*#{@value.tag_name}/mi,
489
+ /#{Liquid::TagStart}-?\s*end#{@value.tag_name}\s*-?#{Liquid::TagEnd}/mi,
490
+ ]
491
+ end
492
+
493
+ # This little algorithm below find the _correct_ block delimiter
494
+ # (endif, endcase, endcomment) for the current tag. What do I
495
+ # mean by correct? It means the one you'd expect. Making sure
496
+ # that we don't do the naive regex find. Since you can have
497
+ # nested ifs, fors, etc.
498
+ #
499
+ # It works by having a stack, pushing onto the stack when we
500
+ # open a tag of our type_name. And popping when we find a
501
+ # closing tag of our type_name.
502
+ #
503
+ # When the stack is empty, we return the end tag match.
504
+ index = start_index
505
+ stack = []
506
+ stack.push("open")
507
+ loop do
508
+ tag_start_match = tag_start.match(source, index)
509
+ tag_end_match = tag_end.match(source, index)
510
+
511
+ return nil unless tag_end_match
512
+
513
+ # We have found a tag_start and it appeared _before_ the
514
+ # tag_end that we found, thus we push it onto the stack.
515
+ if tag_start_match && tag_start_match.end(0) < tag_end_match.end(0)
516
+ stack.push("open")
517
+ end
518
+
519
+ # We have found a tag_end, therefore we pop
520
+ stack.pop
521
+
522
+ # Nothing left on the stack, we're done.
523
+ break tag_end_match if stack.empty?
524
+
525
+ # We keep looking from the end of the end tag we just found.
526
+ index = tag_end_match.end(0)
527
+ end
528
+ end
529
+
530
+ def block_end_match
531
+ @block_end_match ||= find_block_delimiter(block_start_end_index)
532
+ end
290
533
  end
291
534
  end
@@ -14,24 +14,55 @@ module ThemeCheck
14
14
  visit_object(@default, @other, [])
15
15
  end
16
16
 
17
- def add_as_offenses(check, key_prefix: [], node: nil, theme_file: nil)
17
+ def add_as_offenses(check, key_prefix: [], node: nil, theme_file: nil, schema: {})
18
18
  if extra_keys.any?
19
- add_keys_offense(check, "Extra translation keys", extra_keys,
20
- key_prefix: key_prefix, node: node, theme_file: theme_file)
19
+ remove_extra_keys_offense(check, "Extra translation keys", extra_keys,
20
+ key_prefix: key_prefix, node: node, theme_file: theme_file, schema: schema)
21
21
  end
22
22
 
23
23
  if missing_keys.any?
24
- add_keys_offense(check, "Missing translation keys", missing_keys,
25
- key_prefix: key_prefix, node: node, theme_file: theme_file)
24
+ add_missing_keys_offense(check, "Missing translation keys", missing_keys,
25
+ key_prefix: key_prefix, node: node, theme_file: theme_file, schema: schema)
26
26
  end
27
27
  end
28
28
 
29
29
  private
30
30
 
31
- def add_keys_offense(check, cause, keys, key_prefix:, node: nil, theme_file: nil)
32
- message = "#{cause}: #{format_keys(key_prefix, keys)}"
31
+ def remove_extra_keys_offense(check, cause, extra_keys, key_prefix:, node: nil, theme_file: nil, schema: {})
32
+ message = "#{cause}: #{format_keys(key_prefix, extra_keys)}"
33
33
  if node
34
- check.add_offense(message, node: node)
34
+ check.add_offense(message, node: node) do |corrector|
35
+ extra_keys.each do |k|
36
+ SchemaHelper.delete(schema, key_prefix + k)
37
+ end
38
+ corrector.replace_inner_json(node, schema)
39
+ end
40
+ elsif theme_file.is_a?(JsonFile)
41
+ check.add_offense(message, theme_file: theme_file) do |corrector|
42
+ extra_keys.each do |k|
43
+ corrector.remove_translation(theme_file, key_prefix + k)
44
+ end
45
+ end
46
+ else
47
+ check.add_offense(message, theme_file: theme_file)
48
+ end
49
+ end
50
+
51
+ def add_missing_keys_offense(check, cause, missing_keys, key_prefix:, node: nil, theme_file: nil, schema: {})
52
+ message = "#{cause}: #{format_keys(key_prefix, missing_keys)}"
53
+ if node
54
+ check.add_offense(message, node: node) do |corrector|
55
+ missing_keys.each do |k|
56
+ SchemaHelper.set(schema, key_prefix + k, "TODO")
57
+ end
58
+ corrector.replace_inner_json(node, schema)
59
+ end
60
+ elsif theme_file.is_a?(JsonFile)
61
+ check.add_offense(message, theme_file: theme_file) do |corrector|
62
+ missing_keys.each do |k|
63
+ corrector.add_translation(theme_file, key_prefix + k, "TODO")
64
+ end
65
+ end
35
66
  else
36
67
  check.add_offense(message, theme_file: theme_file)
37
68
  end
@@ -30,8 +30,24 @@ module ThemeCheck
30
30
  raise NotImplementedError
31
31
  end
32
32
 
33
+ def start_row
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def start_column
38
+ raise NotImplementedError
39
+ end
40
+
33
41
  def end_index
34
42
  raise NotImplementedError
35
43
  end
44
+
45
+ def end_row
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def end_column
50
+ raise NotImplementedError
51
+ end
36
52
  end
37
53
  end
@@ -39,26 +39,12 @@ module ThemeCheck
39
39
  end
40
40
 
41
41
  @node = node
42
- @theme_file = nil
43
- if node
44
- @theme_file = node.theme_file
45
- elsif theme_file
46
- @theme_file = theme_file
47
- end
48
-
49
- @markup = if markup
50
- markup
51
- else
52
- node&.markup
53
- end
42
+ @theme_file = node&.theme_file || theme_file
43
+ @markup = markup || node&.markup
54
44
 
55
45
  raise ArgumentError, "Offense markup cannot be an empty string" if @markup.is_a?(String) && @markup.empty?
56
46
 
57
- @line_number = if line_number
58
- line_number
59
- elsif @node
60
- @node.line_number
61
- end
47
+ @line_number = line_number || @node&.line_number
62
48
 
63
49
  @position = Position.new(
64
50
  @markup,
@@ -81,11 +67,25 @@ module ThemeCheck
81
67
  end
82
68
  end
83
69
 
70
+ def in_range?(other_range)
71
+ # Zero length ranges are OK and considered the same as size 1 ranges
72
+ other_range = other_range.first..other_range.end if other_range.size == 0 # rubocop:disable Style/ZeroLengthPredicate
73
+ range.cover?(other_range) || other_range.cover?(range)
74
+ end
75
+
76
+ def range
77
+ @range ||= if start_index == end_index
78
+ (start_index..end_index)
79
+ else
80
+ (start_index...end_index) # end_index is excluded
81
+ end
82
+ end
83
+
84
84
  def start_index
85
85
  @position.start_index
86
86
  end
87
87
 
88
- def start_line
88
+ def start_row
89
89
  @position.start_row
90
90
  end
91
91
 
@@ -97,7 +97,7 @@ module ThemeCheck
97
97
  @position.end_index
98
98
  end
99
99
 
100
- def end_line
100
+ def end_row
101
101
  @position.end_row
102
102
  end
103
103
 
@@ -121,6 +121,10 @@ module ThemeCheck
121
121
  StringHelpers.demodulize(check.class.name)
122
122
  end
123
123
 
124
+ def version
125
+ theme_file&.version
126
+ end
127
+
124
128
  def doc
125
129
  check.doc
126
130
  end
@@ -139,9 +143,9 @@ module ThemeCheck
139
143
  !!correction
140
144
  end
141
145
 
142
- def correct
146
+ def correct(corrector = nil)
143
147
  if correctable?
144
- corrector = Corrector.new(theme_file: theme_file)
148
+ corrector ||= Corrector.new(theme_file: theme_file)
145
149
  correction.call(corrector)
146
150
  end
147
151
  rescue => e
@@ -211,9 +215,9 @@ module ThemeCheck
211
215
  check: check.code_name,
212
216
  path: theme_file&.relative_path,
213
217
  severity: check.severity_value,
214
- start_line: start_line,
218
+ start_row: start_row,
215
219
  start_column: start_column,
216
- end_line: end_line,
220
+ end_row: end_row,
217
221
  end_column: end_column,
218
222
  message: message,
219
223
  }
@@ -64,6 +64,10 @@ module ThemeCheck
64
64
  strict_position.end_column
65
65
  end
66
66
 
67
+ def content_line_count
68
+ @content_line_count ||= contents.count("\n")
69
+ end
70
+
67
71
  private
68
72
 
69
73
  def compute_start_offset
@@ -78,10 +82,6 @@ module ThemeCheck
78
82
  @contents
79
83
  end
80
84
 
81
- def content_line_count
82
- @content_line_count ||= contents.count("\n")
83
- end
84
-
85
85
  def line_number
86
86
  return 0 if @line_number_1_indexed.nil?
87
87
  bounded(0, @line_number_1_indexed - 1, content_line_count)
@@ -5,7 +5,7 @@ module ThemeCheck
5
5
  LIQUID_TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
6
6
  LIQUID_VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
7
7
  LIQUID_TAG_OR_VARIABLE = /#{LIQUID_TAG}|#{LIQUID_VARIABLE}/om
8
- HTML_LIQUID_PLACEHOLDER = /≬[0-9a-z]+#*≬/m
8
+ HTML_LIQUID_PLACEHOLDER = /≬[0-9a-z\n]+[#\n]*≬/m
9
9
  START_OR_END_QUOTE = /(^['"])|(['"]$)/
10
10
 
11
11
  def matches(s, re)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ class SchemaHelper
5
+ # Deeply sets a value in a hash. Accepts both arrays and strings for path.
6
+ def self.set(hash, path, value)
7
+ path = path.split('.') if path.is_a?(String)
8
+ path.each_with_index.reduce(hash) do |pointer, (token, index)|
9
+ if index == path.size - 1
10
+ pointer[token] = value
11
+ elsif !pointer.key?(token)
12
+ pointer[token] = {}
13
+ end
14
+ pointer[token]
15
+ end
16
+ hash
17
+ end
18
+
19
+ # Deeply delete a key from a hash
20
+ def self.delete(hash, path)
21
+ path = path.split('.') if path.is_a?(String)
22
+ path.each_with_index.reduce(hash) do |pointer, (token, index)|
23
+ break pointer.delete(token) if index == path.size - 1
24
+ pointer[token]
25
+ end
26
+ end
27
+
28
+ # Deeply add key/values inside a hash.
29
+ #
30
+ # Handles arrays by adding the key/value to all hashes inside the array.
31
+ #
32
+ # Specially handles objects that have the "id" key like this:
33
+ #
34
+ # e.g.
35
+ #
36
+ # schema = {
37
+ # "deep" => [
38
+ # { "id" => "hi" },
39
+ # { "id" => "oh" },
40
+ # ],
41
+ # }
42
+ # assert_equal(
43
+ # {
44
+ # "deep" => [
45
+ # { "id" => "hi", "ho" => "ho" },
46
+ # { "id" => "oh" },
47
+ # ],
48
+ # },
49
+ # SchemaHelper.schema_corrector(schema, "deep.hi.ho", "ho")
50
+ # )
51
+ def self.schema_corrector(schema, path, value)
52
+ return schema unless schema.is_a?(Hash)
53
+ path = path.split('.') if path.is_a?(String)
54
+ path.each_with_index.reduce(schema) do |pointer, (token, index)|
55
+ case pointer
56
+ when Array
57
+ pointer.each do |item|
58
+ schema_corrector(item, path.drop(1), value)
59
+ end
60
+
61
+ when Hash
62
+ break pointer[token] = value if index == path.size - 1
63
+ pointer[token] = {} unless pointer.key?(token) || pointer.key?("id")
64
+ pointer[token].nil? && pointer["id"] == token ? pointer : pointer[token]
65
+ end
66
+ end
67
+ schema
68
+ end
69
+ end
70
+ end
@@ -21,5 +21,9 @@ module ThemeCheck
21
21
  def directories
22
22
  raise NotImplementedError
23
23
  end
24
+
25
+ def versioned?
26
+ false
27
+ end
24
28
  end
25
29
  end
@@ -66,7 +66,6 @@ module ThemeCheck
66
66
 
67
67
  def initialize(tag_name, markup, options)
68
68
  super
69
-
70
69
  if (matches = markup.match(SYNTAX))
71
70
  @liquid_variable_name = matches[:liquid_variable_name]
72
71
  @page_size = parse_expression(matches[:page_size])
@@ -3,7 +3,7 @@ require "pathname"
3
3
 
4
4
  module ThemeCheck
5
5
  class Theme
6
- DEFAULT_LOCALE_REGEXP = %r{^locales/(.*)\.default$}
6
+ DEFAULT_LOCALE_REGEXP = %r{locales/(.*)\.default$}
7
7
  LIQUID_REGEX = /\.liquid$/i
8
8
  JSON_REGEX = /\.json$/i
9
9
 
@@ -3,10 +3,13 @@ require "pathname"
3
3
 
4
4
  module ThemeCheck
5
5
  class ThemeFile
6
+ attr_reader :version, :storage
7
+
6
8
  def initialize(relative_path, storage)
7
9
  @relative_path = relative_path
8
10
  @storage = storage
9
11
  @source = nil
12
+ @version = nil
10
13
  @eol = "\n"
11
14
  end
12
15
 
@@ -36,7 +39,11 @@ module ThemeCheck
36
39
  # source file.
37
40
  def source
38
41
  return @source if @source
39
- @source = @storage.read(@relative_path)
42
+ if @storage.versioned?
43
+ @source, @version = @storage.read_version(@relative_path)
44
+ else
45
+ @source = @storage.read(@relative_path)
46
+ end
40
47
  @eol = @source.include?("\r\n") ? "\r\n" : "\n"
41
48
  @source = @source.gsub("\r\n", "\n")
42
49
  end