theme-check 1.7.0 → 1.9.0

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