theme-check 1.8.0 → 1.9.0

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