theme-check 1.8.0 → 1.9.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +21 -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 +2 -1
  9. data/docs/checks/schema_json_format.md +76 -0
  10. data/docs/language_server/code-action-command-palette.png +0 -0
  11. data/docs/language_server/code-action-flow.png +0 -0
  12. data/docs/language_server/code-action-keyboard.png +0 -0
  13. data/docs/language_server/code-action-light-bulb.png +0 -0
  14. data/docs/language_server/code-action-problem.png +0 -0
  15. data/docs/language_server/code-action-quickfix.png +0 -0
  16. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  17. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  18. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  19. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  20. data/lib/theme_check/checks/default_locale.rb +1 -1
  21. data/lib/theme_check/checks/deprecated_filter.rb +79 -4
  22. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  23. data/lib/theme_check/checks/matching_schema_translations.rb +4 -6
  24. data/lib/theme_check/checks/matching_translations.rb +1 -0
  25. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  26. data/lib/theme_check/checks/missing_template.rb +1 -1
  27. data/lib/theme_check/checks/pagination_size.rb +2 -3
  28. data/lib/theme_check/checks/remote_asset.rb +5 -0
  29. data/lib/theme_check/checks/required_directories.rb +1 -1
  30. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  31. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  32. data/lib/theme_check/checks/translation_key_exists.rb +33 -13
  33. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  34. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  35. data/lib/theme_check/checks/valid_schema.rb +2 -2
  36. data/lib/theme_check/corrector.rb +28 -54
  37. data/lib/theme_check/file_system_storage.rb +4 -3
  38. data/lib/theme_check/html_node.rb +99 -6
  39. data/lib/theme_check/html_visitor.rb +1 -32
  40. data/lib/theme_check/in_memory_storage.rb +9 -0
  41. data/lib/theme_check/json_helpers.rb +14 -0
  42. data/lib/theme_check/language_server/bridge.rb +1 -1
  43. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  44. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  45. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  46. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  47. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  48. data/lib/theme_check/language_server/configuration.rb +69 -0
  49. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  50. data/lib/theme_check/language_server/diagnostics_engine.rb +15 -60
  51. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  52. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  53. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  54. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  55. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  56. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  57. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  58. data/lib/theme_check/language_server/handler.rb +79 -28
  59. data/lib/theme_check/language_server/io_messenger.rb +9 -1
  60. data/lib/theme_check/language_server/server.rb +8 -7
  61. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  62. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  63. data/lib/theme_check/language_server.rb +23 -5
  64. data/lib/theme_check/liquid_node.rb +249 -39
  65. data/lib/theme_check/locale_diff.rb +16 -4
  66. data/lib/theme_check/node.rb +16 -0
  67. data/lib/theme_check/offense.rb +27 -23
  68. data/lib/theme_check/regex_helpers.rb +1 -1
  69. data/lib/theme_check/schema_helper.rb +70 -0
  70. data/lib/theme_check/storage.rb +4 -0
  71. data/lib/theme_check/theme.rb +1 -1
  72. data/lib/theme_check/theme_file.rb +8 -1
  73. data/lib/theme_check/theme_file_rewriter.rb +18 -9
  74. data/lib/theme_check/version.rb +1 -1
  75. data/lib/theme_check.rb +7 -2
  76. metadata +26 -3
  77. 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,36 +106,24 @@ module ThemeCheck
70
106
  position.start_index
71
107
  end
72
108
 
73
- def end_index
74
- position.end_index
75
- end
76
-
77
- def start_token_index
78
- return position.start_index if inside_liquid_tag?
79
- position.start_index - (start_token.length + 1)
109
+ def start_row
110
+ position.start_row
80
111
  end
81
112
 
82
- def end_token_index
83
- return position.end_index if inside_liquid_tag?
84
- position.end_index + end_token.length
113
+ def start_column
114
+ position.start_column
85
115
  end
86
116
 
87
- def render_start_tag
88
- "#{start_token} #{@value.raw}#{end_token}"
89
- end
90
-
91
- def render_end_tag
92
- "#{start_token} #{@value.block_delimiter} #{end_token}"
117
+ def end_index
118
+ position.end_index
93
119
  end
94
120
 
95
- def block_body_start_index
96
- return unless block_tag?
97
- block_regex.begin(:body)
121
+ def end_row
122
+ position.end_row
98
123
  end
99
124
 
100
- def block_body_end_index
101
- return unless block_tag?
102
- block_regex.end(:body)
125
+ def end_column
126
+ position.end_column
103
127
  end
104
128
 
105
129
  # Literals are hard-coded values in the liquid file.
@@ -116,6 +140,14 @@ module ThemeCheck
116
140
  @value.is_a?(Liquid::Variable)
117
141
  end
118
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
+
119
151
  # A {% comment %} block node?
120
152
  def comment?
121
153
  @value.is_a?(Liquid::Comment)
@@ -142,6 +174,10 @@ module ThemeCheck
142
174
  block_tag? || block_body? || document?
143
175
  end
144
176
 
177
+ def schema?
178
+ @value.is_a?(ThemeCheck::Tags::Schema)
179
+ end
180
+
145
181
  # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
146
182
  # and `after_<type_name>` check methods.
147
183
  def type_name
@@ -152,6 +188,86 @@ module ThemeCheck
152
188
  theme_file&.source
153
189
  end
154
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
+
155
271
  WHITESPACE = /\s/
156
272
 
157
273
  # Is this node inside a `{% liquid ... %}` block?
@@ -193,30 +309,41 @@ module ThemeCheck
193
309
  end
194
310
 
195
311
  def start_token
196
- return "" if inside_liquid_tag?
197
- output = ""
198
- output += "{{" if variable?
199
- output += "{%" if tag?
200
- output += "-" if whitespace_trimmed_start?
201
- 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
202
325
  end
203
326
 
204
327
  def end_token
205
- return "" if inside_liquid_tag?
206
- output = ""
207
- output += "-" if whitespace_trimmed_end?
208
- output += "}}" if variable?
209
- output += "%}" if tag?
210
- 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
211
343
  end
212
344
 
213
345
  private
214
346
 
215
- def block_regex
216
- return unless block_tag?
217
- /(?<start_token>#{render_start_tag})(?<body>.*)(?<end_token>#{render_end_tag})/m.match(source)
218
- end
219
-
220
347
  def position
221
348
  @position ||= Position.new(
222
349
  markup,
@@ -225,6 +352,22 @@ module ThemeCheck
225
352
  )
226
353
  end
227
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
+
228
371
  # Here we're hacking around a glorious bug in Liquid that makes it so the
229
372
  # line_number and markup of a tag is wrong if there's whitespace
230
373
  # between the tag_name and the markup of the tag.
@@ -320,5 +463,72 @@ module ThemeCheck
320
463
  # return the real raw content
321
464
  @tag_markup = source[tag_start...markup_end]
322
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
323
533
  end
324
534
  end
@@ -33,9 +33,15 @@ module ThemeCheck
33
33
  if node
34
34
  check.add_offense(message, node: node) do |corrector|
35
35
  extra_keys.each do |k|
36
- corrector.remove_key(schema, key_prefix + 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)
37
44
  end
38
- corrector.replace_block_body(node, schema)
39
45
  end
40
46
  else
41
47
  check.add_offense(message, theme_file: theme_file)
@@ -47,9 +53,15 @@ module ThemeCheck
47
53
  if node
48
54
  check.add_offense(message, node: node) do |corrector|
49
55
  missing_keys.each do |k|
50
- corrector.add_key(schema, key_prefix + k, "TODO")
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")
51
64
  end
52
- corrector.replace_block_body(node, schema)
53
65
  end
54
66
  else
55
67
  check.add_offense(message, theme_file: theme_file)
@@ -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
  }
@@ -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
@@ -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
@@ -11,36 +11,45 @@ module ThemeCheck
11
11
  )
12
12
  end
13
13
 
14
- def insert_before(node, content)
14
+ def insert_before(node, content, character_range = nil)
15
15
  @rewriter.insert_before(
16
- range(node.start_index, node.end_index),
16
+ range(
17
+ character_range&.begin || node.start_index,
18
+ character_range&.end || node.end_index,
19
+ ),
17
20
  content
18
21
  )
19
22
  end
20
23
 
21
- def insert_after(node, content)
24
+ def insert_after(node, content, character_range = nil)
22
25
  @rewriter.insert_after(
23
- range(node.start_index, node.end_index),
26
+ range(
27
+ character_range&.begin || node.start_index,
28
+ character_range&.end || node.end_index,
29
+ ),
24
30
  content
25
31
  )
26
32
  end
27
33
 
28
34
  def remove(node)
29
35
  @rewriter.remove(
30
- range(node.start_token_index, node.end_token_index)
36
+ range(node.outer_markup_start_index, node.outer_markup_end_index)
31
37
  )
32
38
  end
33
39
 
34
- def replace(node, content)
40
+ def replace(node, content, character_range = nil)
35
41
  @rewriter.replace(
36
- range(node.start_index, node.end_index),
42
+ range(
43
+ character_range&.begin || node.start_index,
44
+ character_range&.end || node.end_index,
45
+ ),
37
46
  content
38
47
  )
39
48
  end
40
49
 
41
- def replace_body(node, content)
50
+ def replace_inner_markup(node, content)
42
51
  @rewriter.replace(
43
- range(node.block_body_start_index, node.block_body_end_index),
52
+ range(node.inner_markup_start_index, node.inner_markup_end_index),
44
53
  content
45
54
  )
46
55
  end