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
@@ -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