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
@@ -17,7 +17,16 @@ module ThemeCheck
17
17
  triggerCharacters: ['.', '{{ ', '{% '],
18
18
  context: true,
19
19
  },
20
+ codeActionProvider: {
21
+ codeActionKinds: CodeActionProvider.all.map(&:kind),
22
+ resolveProvider: false,
23
+ workDoneProgress: false,
24
+ },
20
25
  documentLinkProvider: true,
26
+ executeCommandProvider: {
27
+ workDoneProgress: false,
28
+ commands: ExecuteCommandProvider.all.map(&:command),
29
+ },
21
30
  textDocumentSync: {
22
31
  openClose: true,
23
32
  change: TextDocumentSyncKind::FULL,
@@ -36,17 +45,29 @@ module ThemeCheck
36
45
  # Tell the client we don't support anything if there's no rootPath
37
46
  return @bridge.send_response(id, { capabilities: {} }) if @root_path.nil?
38
47
 
39
- @bridge.supports_work_done_progress = params.dig('capabilities', 'window', 'workDoneProgress') || false
48
+ @client_capabilities = ClientCapabilities.new(params.dig(:capabilities) || {})
49
+ @configuration = Configuration.new(@bridge, @client_capabilities)
50
+ @bridge.supports_work_done_progress = @client_capabilities.supports_work_done_progress?
40
51
  @storage = in_memory_storage(@root_path)
52
+ @diagnostics_manager = DiagnosticsManager.new
41
53
  @completion_engine = CompletionEngine.new(@storage)
42
54
  @document_link_engine = DocumentLinkEngine.new(@storage)
43
- @diagnostics_engine = DiagnosticsEngine.new(@bridge)
55
+ @diagnostics_engine = DiagnosticsEngine.new(@storage, @bridge, @diagnostics_manager)
56
+ @execute_command_engine = ExecuteCommandEngine.new
57
+ @execute_command_engine << CorrectionExecuteCommandProvider.new(@storage, @bridge, @diagnostics_manager)
58
+ @execute_command_engine << RunChecksExecuteCommandProvider.new(@diagnostics_engine, @root_path, config_for_path(@root_path))
59
+ @code_action_engine = CodeActionEngine.new(@storage, @diagnostics_manager)
44
60
  @bridge.send_response(id, {
45
61
  capabilities: CAPABILITIES,
46
62
  serverInfo: SERVER_INFO,
47
63
  })
48
64
  end
49
65
 
66
+ def on_initialized(_id, _params)
67
+ @configuration.fetch
68
+ @configuration.register_did_change_capability
69
+ end
70
+
50
71
  def on_shutdown(id, _params)
51
72
  @bridge.send_response(id, nil)
52
73
  end
@@ -55,36 +76,63 @@ module ThemeCheck
55
76
  close!
56
77
  end
57
78
 
58
- def on_text_document_did_change(_id, params)
79
+ def on_text_document_did_open(_id, params)
59
80
  relative_path = relative_path_from_text_document_uri(params)
60
- @storage.write(relative_path, content_changes_text(params))
81
+ @storage.write(relative_path, text_document_text(params), text_document_version(params))
82
+ analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_open?
61
83
  end
62
84
 
63
- def on_text_document_did_close(_id, params)
85
+ def on_text_document_did_change(_id, params)
64
86
  relative_path = relative_path_from_text_document_uri(params)
65
- @storage.write(relative_path, "")
87
+ @storage.write(relative_path, content_changes_text(params), text_document_version(params))
88
+ analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_change?
66
89
  end
67
90
 
68
- def on_text_document_did_open(_id, params)
91
+ def on_text_document_did_close(_id, params)
69
92
  relative_path = relative_path_from_text_document_uri(params)
70
- @storage.write(relative_path, text_document_text(params))
71
- analyze_and_send_offenses(text_document_uri(params)) if @diagnostics_engine.first_run?
93
+ file_system_content = Pathname.new(text_document_uri(params)).read(mode: 'rb', encoding: 'UTF-8')
94
+ # On close, the file system becomes the source of truth
95
+ @storage.write(relative_path, file_system_content, nil)
72
96
  end
73
97
 
74
98
  def on_text_document_did_save(_id, params)
75
- analyze_and_send_offenses(text_document_uri(params))
99
+ analyze_and_send_offenses(text_document_uri(params)) if @configuration.check_on_save?
76
100
  end
77
101
 
78
102
  def on_text_document_document_link(id, params)
79
103
  relative_path = relative_path_from_text_document_uri(params)
80
- @bridge.send_response(id, document_links(relative_path))
104
+ @bridge.send_response(id, @document_link_engine.document_links(relative_path))
81
105
  end
82
106
 
83
107
  def on_text_document_completion(id, params)
84
108
  relative_path = relative_path_from_text_document_uri(params)
85
- line = params.dig('position', 'line')
86
- col = params.dig('position', 'character')
87
- @bridge.send_response(id, completions(relative_path, line, col))
109
+ line = params.dig(:position, :line)
110
+ col = params.dig(:position, :character)
111
+ @bridge.send_response(id, @completion_engine.completions(relative_path, line, col))
112
+ end
113
+
114
+ def on_text_document_code_action(id, params)
115
+ absolute_path = text_document_uri(params)
116
+ start_position = range_element(params, :start)
117
+ end_position = range_element(params, :end)
118
+ only_code_action_kinds = params.dig(:context, :only) || []
119
+ @bridge.send_response(id, @code_action_engine.code_actions(
120
+ absolute_path,
121
+ start_position,
122
+ end_position,
123
+ only_code_action_kinds,
124
+ ))
125
+ end
126
+
127
+ def on_workspace_execute_command(id, params)
128
+ @bridge.send_response(id, @execute_command_engine.execute(
129
+ params[:command],
130
+ params[:arguments],
131
+ ))
132
+ end
133
+
134
+ def on_workspace_did_change_configuration(_id, _params)
135
+ @configuration.fetch(force: true)
88
136
  end
89
137
 
90
138
  private
@@ -98,16 +146,16 @@ module ThemeCheck
98
146
  ignored_patterns: config.ignored_patterns
99
147
  )
100
148
 
101
- # Turn that into a hash of empty buffers
149
+ # Turn that into a hash of buffers
102
150
  files = fs.files
103
- .map { |fn| [fn, ""] }
151
+ .map { |fn| [fn, fs.read(fn)] }
104
152
  .to_h
105
153
 
106
- InMemoryStorage.new(files, config.root)
154
+ VersionedInMemoryStorage.new(files, config.root)
107
155
  end
108
156
 
109
157
  def text_document_uri(params)
110
- file_path(params.dig('textDocument', 'uri'))
158
+ file_path(params.dig(:textDocument, :uri))
111
159
  end
112
160
 
113
161
  def relative_path_from_text_document_uri(params)
@@ -115,8 +163,8 @@ module ThemeCheck
115
163
  end
116
164
 
117
165
  def root_path_from_params(params)
118
- root_uri = params["rootUri"]
119
- root_path = params["rootPath"]
166
+ root_uri = params[:rootUri]
167
+ root_path = params[:rootPath]
120
168
  if root_uri
121
169
  file_path(root_uri)
122
170
  elsif root_path
@@ -125,11 +173,15 @@ module ThemeCheck
125
173
  end
126
174
 
127
175
  def text_document_text(params)
128
- params.dig('textDocument', 'text')
176
+ params.dig(:textDocument, :text)
177
+ end
178
+
179
+ def text_document_version(params)
180
+ params.dig(:textDocument, :version)
129
181
  end
130
182
 
131
183
  def content_changes_text(params)
132
- params.dig('contentChanges', 0, 'text')
184
+ params.dig(:contentChanges, 0, :text)
133
185
  end
134
186
 
135
187
  def config_for_path(path)
@@ -144,12 +196,11 @@ module ThemeCheck
144
196
  )
145
197
  end
146
198
 
147
- def completions(relative_path, line, col)
148
- @completion_engine.completions(relative_path, line, col)
149
- end
150
-
151
- def document_links(relative_path)
152
- @document_link_engine.document_links(relative_path)
199
+ def range_element(params, start_or_end)
200
+ [
201
+ params.dig(:range, start_or_end, :line),
202
+ params.dig(:range, start_or_end, :character),
203
+ ]
153
204
  end
154
205
 
155
206
  def log(message)
@@ -3,10 +3,18 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class IOMessenger < Messenger
6
+ def self.err_stream
7
+ if ThemeCheck.debug_log_file
8
+ File.open(ThemeCheck.debug_log_file, "w")
9
+ else
10
+ STDERR
11
+ end
12
+ end
13
+
6
14
  def initialize(
7
15
  in_stream: STDIN,
8
16
  out_stream: STDOUT,
9
- err_stream: STDERR
17
+ err_stream: IOMessenger.err_stream
10
18
  )
11
19
  validate!([in_stream, out_stream, err_stream])
12
20
 
@@ -55,9 +55,9 @@ module ThemeCheck
55
55
  @json_rpc_thread = Thread.new do
56
56
  loop do
57
57
  message = @bridge.read_message
58
- if message['method'] == 'initialize'
58
+ if message[:method] == 'initialize'
59
59
  handle_message(message)
60
- elsif message.key?('result')
60
+ elsif message.key?(:result)
61
61
  # Responses are handled on the main thread to prevent
62
62
  # a potential deadlock caused by all handlers waiting
63
63
  # for a responses.
@@ -101,10 +101,10 @@ module ThemeCheck
101
101
  private
102
102
 
103
103
  def handle_message(message)
104
- id = message['id']
105
- method_name = message['method']
104
+ id = message[:id]
105
+ method_name = message[:method]
106
106
  method_name &&= "on_#{to_snake_case(method_name)}"
107
- params = message['params']
107
+ params = message[:params]
108
108
 
109
109
  if @handler.respond_to?(method_name)
110
110
  @handler.send(method_name, id, params)
@@ -117,12 +117,13 @@ module ThemeCheck
117
117
  raise e unless is_request
118
118
  # Errors obtained in request handlers should be sent
119
119
  # back as internal errors instead of closing the program.
120
+ @bridge.log("#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
120
121
  @bridge.send_internal_error(id, e)
121
122
  end
122
123
 
123
124
  def handle_response(message)
124
- id = message['id']
125
- result = message['result']
125
+ id = message[:id]
126
+ result = message[:result]
126
127
  @bridge.receive_response(id, result)
127
128
  end
128
129
 
@@ -15,6 +15,7 @@ module ThemeCheck
15
15
  #
16
16
  # Exists because of https://github.com/Shopify/theme-check/issues/360
17
17
  def file_uri(absolute_path)
18
+ return if absolute_path.nil?
18
19
  "file://" + absolute_path
19
20
  .to_s
20
21
  .split('/')
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ class VersionedInMemoryStorage < InMemoryStorage
5
+ Version = Struct.new(:id, :version)
6
+
7
+ attr_reader :versions
8
+
9
+ def initialize(files, root = "/dev/null")
10
+ super(files, root)
11
+ @versions = {}
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ # Motivations:
16
+ # - Need way for LanguageServer to know on which version of a file
17
+ # the check was run on, because we need to know where the
18
+ # TextEdit goes. If the text changed, our TextEdit might not be
19
+ # in the right spot. e.g.
20
+ #
21
+ # Example:
22
+ #
23
+ # ```
24
+ # Hi
25
+ # {{world}}
26
+ # ```
27
+ #
28
+ # Would produce two "SpaceInsideBrace" errors:
29
+ #
30
+ # - One after {{ at index 5 to 6
31
+ # - One before }} at index 10 to 11
32
+ #
33
+ # If the user goes in and changes Hi to Sup, and _then_
34
+ # right clicks to apply the code edit at index 5 to 6, he'd
35
+ # get the following:
36
+ #
37
+ # ```
38
+ # Sup
39
+ # { {world}}
40
+ # ```
41
+ #
42
+ # Which is not a fix at all.
43
+ #
44
+ # Solution:
45
+ # - Have the LanguageServer store the version on textDocument/did{Open,Change,Close}
46
+ # - Have ThemeFile store the version right after @storage.read.
47
+ # - Add version to the diagnostic meta data
48
+ # - Use diagnostic meta data to determine if we can make a code edit or not
49
+ # - Only offer fixes on "clean" files (or offer the change but specify the version so the editor knows what to do with it)
50
+ def write(relative_path, content, version)
51
+ @mutex.synchronize do
52
+ @versions[relative_path] = version
53
+ super(relative_path, content)
54
+ end
55
+ end
56
+
57
+ def read_version(relative_path)
58
+ @mutex.synchronize { [read(relative_path), version(relative_path)] }
59
+ end
60
+
61
+ def versioned?
62
+ true
63
+ end
64
+
65
+ def version(relative_path)
66
+ @versions[relative_path.to_s]
67
+ end
68
+ end
69
+ end
@@ -1,31 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "language_server/protocol"
3
3
  require_relative "language_server/constants"
4
+ require_relative "language_server/configuration"
4
5
  require_relative "language_server/channel"
5
6
  require_relative "language_server/messenger"
6
7
  require_relative "language_server/io_messenger"
7
8
  require_relative "language_server/bridge"
8
9
  require_relative "language_server/uri_helper"
9
- require_relative "language_server/handler"
10
10
  require_relative "language_server/server"
11
11
  require_relative "language_server/tokens"
12
12
  require_relative "language_server/variable_lookup_finder"
13
+ require_relative "language_server/diagnostic"
14
+ require_relative "language_server/diagnostics_manager"
15
+ require_relative "language_server/diagnostics_engine"
16
+ require_relative "language_server/document_change_corrector"
17
+ require_relative "language_server/versioned_in_memory_storage"
18
+ require_relative "language_server/client_capabilities"
19
+
13
20
  require_relative "language_server/completion_helper"
14
21
  require_relative "language_server/completion_provider"
15
22
  require_relative "language_server/completion_engine"
23
+ Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
24
+ require file
25
+ end
26
+
16
27
  require_relative "language_server/document_link_provider"
17
28
  require_relative "language_server/document_link_engine"
18
- require_relative "language_server/diagnostics_tracker"
19
- require_relative "language_server/diagnostics_engine"
29
+ Dir[__dir__ + "/language_server/document_link_providers/*.rb"].each do |file|
30
+ require file
31
+ end
20
32
 
21
- Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
33
+ require_relative "language_server/execute_command_provider"
34
+ require_relative "language_server/execute_command_engine"
35
+ Dir[__dir__ + "/language_server/execute_command_providers/*.rb"].each do |file|
22
36
  require file
23
37
  end
24
38
 
25
- Dir[__dir__ + "/language_server/document_link_providers/*.rb"].each do |file|
39
+ require_relative "language_server/code_action_provider"
40
+ require_relative "language_server/code_action_engine"
41
+ Dir[__dir__ + "/language_server/code_action_providers/*.rb"].each do |file|
26
42
  require file
27
43
  end
28
44
 
45
+ require_relative "language_server/handler"
46
+
29
47
  module ThemeCheck
30
48
  module LanguageServer
31
49
  def self.start