theme-check 1.7.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +49 -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/data/shopify_liquid/tags.yml +9 -9
  10. data/docs/checks/TEMPLATE.md.erb +24 -19
  11. data/docs/checks/schema_json_format.md +76 -0
  12. data/docs/language_server/code-action-command-palette.png +0 -0
  13. data/docs/language_server/code-action-flow.png +0 -0
  14. data/docs/language_server/code-action-keyboard.png +0 -0
  15. data/docs/language_server/code-action-light-bulb.png +0 -0
  16. data/docs/language_server/code-action-problem.png +0 -0
  17. data/docs/language_server/code-action-quickfix.png +0 -0
  18. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  19. data/exe/theme-check-language-server +0 -4
  20. data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
  21. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
  22. data/lib/theme_check/checks/asset_url_filters.rb +2 -0
  23. data/lib/theme_check/checks/default_locale.rb +1 -1
  24. data/lib/theme_check/checks/deprecated_filter.rb +79 -4
  25. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
  26. data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
  27. data/lib/theme_check/checks/matching_translations.rb +1 -0
  28. data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
  29. data/lib/theme_check/checks/missing_template.rb +1 -1
  30. data/lib/theme_check/checks/pagination_size.rb +2 -3
  31. data/lib/theme_check/checks/remote_asset.rb +5 -0
  32. data/lib/theme_check/checks/required_directories.rb +1 -1
  33. data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
  34. data/lib/theme_check/checks/schema_json_format.rb +29 -0
  35. data/lib/theme_check/checks/space_inside_braces.rb +132 -87
  36. data/lib/theme_check/checks/translation_key_exists.rb +33 -25
  37. data/lib/theme_check/checks/unused_assign.rb +3 -2
  38. data/lib/theme_check/checks/unused_snippet.rb +1 -1
  39. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  40. data/lib/theme_check/checks/valid_schema.rb +2 -2
  41. data/lib/theme_check/corrector.rb +34 -23
  42. data/lib/theme_check/exceptions.rb +1 -0
  43. data/lib/theme_check/file_system_storage.rb +8 -3
  44. data/lib/theme_check/html_node.rb +99 -6
  45. data/lib/theme_check/html_visitor.rb +1 -32
  46. data/lib/theme_check/in_memory_storage.rb +9 -0
  47. data/lib/theme_check/json_helpers.rb +14 -0
  48. data/lib/theme_check/language_server/bridge.rb +142 -0
  49. data/lib/theme_check/language_server/channel.rb +69 -0
  50. data/lib/theme_check/language_server/client_capabilities.rb +27 -0
  51. data/lib/theme_check/language_server/code_action_engine.rb +32 -0
  52. data/lib/theme_check/language_server/code_action_provider.rb +42 -0
  53. data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
  54. data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
  55. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
  56. data/lib/theme_check/language_server/configuration.rb +69 -0
  57. data/lib/theme_check/language_server/diagnostic.rb +124 -0
  58. data/lib/theme_check/language_server/diagnostics_engine.rb +80 -0
  59. data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
  60. data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
  61. data/lib/theme_check/language_server/document_link_provider.rb +6 -6
  62. data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
  63. data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
  64. data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  65. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
  66. data/lib/theme_check/language_server/handler.rb +92 -217
  67. data/lib/theme_check/language_server/io_messenger.rb +112 -0
  68. data/lib/theme_check/language_server/messenger.rb +12 -42
  69. data/lib/theme_check/language_server/protocol.rb +4 -0
  70. data/lib/theme_check/language_server/server.rb +54 -110
  71. data/lib/theme_check/language_server/uri_helper.rb +1 -0
  72. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
  73. data/lib/theme_check/language_server.rb +28 -6
  74. data/lib/theme_check/liquid_node.rb +255 -12
  75. data/lib/theme_check/locale_diff.rb +48 -10
  76. data/lib/theme_check/node.rb +16 -0
  77. data/lib/theme_check/offense.rb +27 -23
  78. data/lib/theme_check/position.rb +4 -4
  79. data/lib/theme_check/regex_helpers.rb +1 -1
  80. data/lib/theme_check/schema_helper.rb +70 -0
  81. data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
  82. data/lib/theme_check/shopify_liquid/tag.rb +19 -1
  83. data/lib/theme_check/shopify_liquid.rb +1 -0
  84. data/lib/theme_check/storage.rb +4 -0
  85. data/lib/theme_check/tags.rb +0 -1
  86. data/lib/theme_check/theme.rb +1 -1
  87. data/lib/theme_check/theme_file.rb +8 -1
  88. data/lib/theme_check/theme_file_rewriter.rb +28 -6
  89. data/lib/theme_check/version.rb +1 -1
  90. data/lib/theme_check.rb +11 -2
  91. metadata +31 -3
  92. data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentChangeCorrector
6
+ include URIHelper
7
+ include JsonHelpers
8
+
9
+ def initialize
10
+ @json_edits = {}
11
+ @json_file_edits = {}
12
+ @text_document_edits = {}
13
+ @create_files = []
14
+ @rename_files = []
15
+ @delete_files = []
16
+ end
17
+
18
+ def document_changes
19
+ apply_json_edits
20
+ apply_json_file_edits
21
+ @create_files + @rename_files + @text_document_edits.values + @delete_files
22
+ end
23
+
24
+ # @param node [Node]
25
+ def insert_before(node, content, character_range = nil)
26
+ position = character_range_position(node, character_range) if character_range
27
+ edits(node) << {
28
+ range: {
29
+ start: start_location(position || node),
30
+ end: start_location(position || node),
31
+ },
32
+ newText: content,
33
+ }
34
+ end
35
+
36
+ # @param node [Node]
37
+ def insert_after(node, content, character_range = nil)
38
+ position = character_range_position(node, character_range) if character_range
39
+ edits(node) << {
40
+ range: {
41
+ start: end_location(position || node),
42
+ end: end_location(position || node),
43
+ },
44
+ newText: content,
45
+ }
46
+ end
47
+
48
+ def replace(node, content, character_range = nil)
49
+ position = character_range_position(node, character_range) if character_range
50
+ edits(node) << {
51
+ range: range(position || node),
52
+ newText: content,
53
+ }
54
+ end
55
+
56
+ # @param node [LiquidNode]
57
+ def remove(node)
58
+ edits(node) << {
59
+ range: {
60
+ start: { line: node.outer_markup_start_row, character: node.outer_markup_start_column },
61
+ end: { line: node.outer_markup_end_row, character: node.outer_markup_end_column },
62
+ },
63
+ newText: '',
64
+ }
65
+ end
66
+
67
+ def replace_inner_markup(node, content)
68
+ edits(node) << {
69
+ range: {
70
+ start: {
71
+ line: node.inner_markup_start_row,
72
+ character: node.inner_markup_start_column,
73
+ },
74
+ end: {
75
+ line: node.inner_markup_end_row,
76
+ character: node.inner_markup_end_column,
77
+ },
78
+ },
79
+ newText: content,
80
+ }
81
+ end
82
+
83
+ def replace_inner_json(node, json, **pretty_json_opts)
84
+ # Kind of brittle alert: We're assuming that modifications are
85
+ # made directly on the same json hash (e.g. schema). As such,
86
+ # if this assumption is true, then it follows that the
87
+ # "correct" JSON is the _last_ one that we defined.
88
+ #
89
+ # We're going to append those changes to the text edit when
90
+ # we're done.
91
+ #
92
+ # We're doing this because no language client will accept
93
+ # text modifications that occur on the same range. So we need
94
+ # to dedup our JSON edits for the client to accept our change.
95
+ #
96
+ # What we're doing here is overwriting the json edit for a
97
+ # node to the latest one that is called. If all the edits
98
+ # occur on the same hash, this final hash will have all the
99
+ # edits in it.
100
+ @json_edits[node] = [json, pretty_json_opts]
101
+ end
102
+
103
+ def wrap(node, insert_before, insert_after)
104
+ edits(node) << {
105
+ range: range(node),
106
+ newText: insert_before + node.markup + insert_after,
107
+ }
108
+ end
109
+
110
+ def create_file(storage, relative_path, contents = nil, overwrite: false)
111
+ uri = file_uri(storage.path(relative_path))
112
+ @create_files << create_file_change(uri, overwrite)
113
+ return if contents.nil?
114
+ text_document = { uri: uri, version: nil }
115
+ @text_document_edits[text_document] = {
116
+ textDocument: text_document,
117
+ edits: [{
118
+ range: {
119
+ start: { line: 0, character: 0 },
120
+ end: { line: 0, character: 0 },
121
+ },
122
+ newText: contents,
123
+ }],
124
+ }
125
+ end
126
+
127
+ def remove_file(storage, relative_path)
128
+ uri = file_uri(storage.path(relative_path))
129
+ @delete_files << delete_file_change(uri)
130
+ end
131
+
132
+ def mkdir(storage, relative_path)
133
+ path = Pathname.new(relative_path).join("tmp").to_s
134
+ # The LSP doesn't have a concept for directories, so what we
135
+ # do is create a file and then delete it.
136
+ #
137
+ # It does the job :upside_down_smile:.
138
+ create_file(storage, path)
139
+ remove_file(storage, path)
140
+ end
141
+
142
+ def add_translation(file, path, value)
143
+ raise ArgumentError unless file.is_a?(JsonFile)
144
+ hash = file.content
145
+ SchemaHelper.set(hash, path, value)
146
+ @json_file_edits[file] = hash
147
+ end
148
+
149
+ def remove_translation(file, path)
150
+ raise ArgumentError unless file.is_a?(JsonFile)
151
+ hash = file.content
152
+ SchemaHelper.delete(hash, path)
153
+ @json_file_edits[file] = hash
154
+ end
155
+
156
+ private
157
+
158
+ def apply_json_edits
159
+ @json_edits.each do |node, (json, pretty_json_opts)|
160
+ replace_inner_markup(node, pretty_json(json, **pretty_json_opts))
161
+ end
162
+ end
163
+
164
+ def apply_json_file_edits
165
+ @json_file_edits.each do |file, hash|
166
+ replace_entire_file(file, JSON.pretty_generate(hash))
167
+ end
168
+ end
169
+
170
+ def replace_entire_file(file, contents)
171
+ text_document = to_text_document(file)
172
+ position = ThemeCheck::StrictPosition.new(file.source, file.source, 0)
173
+ @text_document_edits[text_document] = {
174
+ textDocument: text_document,
175
+ edits: [{
176
+ range: {
177
+ start: { line: 0, character: 0 },
178
+ end: { line: position.end_row, character: position.end_column },
179
+ },
180
+ newText: contents,
181
+ }],
182
+ }
183
+ end
184
+
185
+ # @param node [Node]
186
+ def text_document_edit(node)
187
+ text_document = to_text_document(node)
188
+ @text_document_edits[text_document] ||= {
189
+ textDocument: text_document,
190
+ edits: [],
191
+ }
192
+ end
193
+
194
+ def create_file_change(uri, overwrite = false)
195
+ change = {}
196
+ change[:kind] = 'create'
197
+ change[:uri] = uri
198
+ change[:options] = { overwrite: overwrite } if overwrite
199
+ change
200
+ end
201
+
202
+ def delete_file_change(uri)
203
+ {
204
+ kind: 'delete',
205
+ uri: uri,
206
+ }
207
+ end
208
+
209
+ def edits(node)
210
+ text_document_edit(node)[:edits]
211
+ end
212
+
213
+ def to_text_document(thing)
214
+ case thing
215
+ when Node
216
+ {
217
+ uri: file_uri(thing.theme_file&.path),
218
+ version: thing.theme_file&.version,
219
+ }
220
+ when ThemeFile
221
+ {
222
+ uri: file_uri(thing.path),
223
+ version: thing.version,
224
+ }
225
+ else
226
+ raise ArgumentError
227
+ end
228
+ end
229
+
230
+ def absolute_path(node)
231
+ node.theme_file&.path
232
+ end
233
+
234
+ def character_range_position(node, character_range)
235
+ return unless character_range
236
+ source = node.theme_file.source
237
+ StrictPosition.new(
238
+ source[character_range],
239
+ source,
240
+ character_range.begin,
241
+ )
242
+ end
243
+
244
+ # @param node [ThemeCheck::Node]
245
+ def range(node)
246
+ {
247
+ start: start_location(node),
248
+ end: end_location(node),
249
+ }
250
+ end
251
+
252
+ def start_location(node)
253
+ {
254
+ line: node.start_row,
255
+ character: node.start_column,
256
+ }
257
+ end
258
+
259
+ def end_location(node)
260
+ {
261
+ line: node.end_row,
262
+ character: node.end_column,
263
+ }
264
+ end
265
+ end
266
+ end
267
+ end
@@ -37,12 +37,12 @@ module ThemeCheck
37
37
 
38
38
  def document_links(buffer)
39
39
  matches(buffer, partial_regexp).map do |match|
40
- start_line, start_character = from_index_to_row_column(
40
+ start_row, start_column = from_index_to_row_column(
41
41
  buffer,
42
42
  match.begin(:partial),
43
43
  )
44
44
 
45
- end_line, end_character = from_index_to_row_column(
45
+ end_row, end_column = from_index_to_row_column(
46
46
  buffer,
47
47
  match.end(:partial)
48
48
  )
@@ -51,12 +51,12 @@ module ThemeCheck
51
51
  target: file_link(match[:partial]),
52
52
  range: {
53
53
  start: {
54
- line: start_line,
55
- character: start_character,
54
+ line: start_row,
55
+ character: start_column,
56
56
  },
57
57
  end: {
58
- line: end_line,
59
- character: end_character,
58
+ line: end_row,
59
+ character: end_column,
60
60
  },
61
61
  },
62
62
  }
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ExecuteCommandEngine
6
+ def initialize
7
+ @providers = {}
8
+ end
9
+
10
+ def <<(provider)
11
+ @providers[provider.command] = provider
12
+ end
13
+
14
+ def execute(command, arguments)
15
+ @providers[command].execute(arguments)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ExecuteCommandProvider
6
+ class << self
7
+ def all
8
+ @all ||= []
9
+ end
10
+
11
+ def inherited(subclass)
12
+ all << subclass
13
+ end
14
+
15
+ def command(cmd = nil)
16
+ @command = cmd unless cmd.nil?
17
+ @command
18
+ end
19
+ end
20
+
21
+ def execute(arguments)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def command
26
+ self.class.command
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class CorrectionExecuteCommandProvider < ExecuteCommandProvider
6
+ include URIHelper
7
+
8
+ command "correction"
9
+
10
+ attr_reader :storage, :bridge, :diagnostics_manager
11
+
12
+ def initialize(storage, bridge, diagnostics_manager)
13
+ @storage = storage
14
+ @bridge = bridge
15
+ @diagnostics_manager = diagnostics_manager
16
+ end
17
+
18
+ # The arguments passed to this method are the ones forwarded
19
+ # from the selected CodeAction by the client.
20
+ #
21
+ # @param diagnostic_hashes [Array] - of diagnostics
22
+ def execute(diagnostic_hashes)
23
+ # attempt to apply the document changes
24
+ workspace_edit = diagnostics_manager.workspace_edit(diagnostic_hashes)
25
+ result = bridge.send_request('workspace/applyEdit', {
26
+ label: 'Theme Check correction',
27
+ edit: workspace_edit,
28
+ })
29
+
30
+ # Bail if unable to apply changes
31
+ return unless result[:applied]
32
+
33
+ # Clean up internal representation of fixed diagnostics
34
+ diagnostics_update = diagnostics_manager.delete_applied(diagnostic_hashes)
35
+
36
+ # Send updated diagnostics to client
37
+ diagnostics_update
38
+ .map do |relative_path, diagnostics|
39
+ bridge.send_notification('textDocument/publishDiagnostics', {
40
+ uri: file_uri(storage.path(relative_path)),
41
+ diagnostics: diagnostics.map(&:to_h),
42
+ })
43
+ storage.path(relative_path)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class RunChecksExecuteCommandProvider < ExecuteCommandProvider
6
+ include URIHelper
7
+
8
+ command "runChecks"
9
+
10
+ def initialize(diagnostics_engine, root_path, root_config)
11
+ @diagnostics_engine = diagnostics_engine
12
+ @root_path = root_path
13
+ @root_config = root_config
14
+ end
15
+
16
+ def execute(_args)
17
+ @diagnostics_engine.analyze_and_send_offenses(@root_path, @root_config, force: true)
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end