theme-check 1.10.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/cla.yml +22 -0
  5. data/.github/workflows/theme-check.yml +1 -1
  6. data/CHANGELOG.md +58 -0
  7. data/README.md +7 -8
  8. data/config/default.yml +5 -0
  9. data/config/theme_app_extension.yml +1 -0
  10. data/data/shopify_liquid/filters.yml +18 -0
  11. data/data/shopify_liquid/theme_app_extension_objects.yml +2 -0
  12. data/dev.yml +1 -1
  13. data/docs/api/check.md +1 -1
  14. data/docs/checks/TEMPLATE.md.erb +1 -1
  15. data/docs/checks/asset_preload.md +60 -0
  16. data/docs/checks/asset_size_javascript.md +2 -2
  17. data/docs/checks/missing_enable_comment.md +3 -3
  18. data/docs/checks/nested_snippet.md +8 -8
  19. data/docs/checks/schema_json_format.md +1 -1
  20. data/docs/checks/translation_key_exists.md +4 -4
  21. data/docs/checks/valid_html_translation.md +1 -1
  22. data/lib/theme_check/analyzer.rb +18 -3
  23. data/lib/theme_check/check.rb +6 -1
  24. data/lib/theme_check/checks/asset_preload.rb +20 -0
  25. data/lib/theme_check/checks/deprecated_filter.rb +29 -5
  26. data/lib/theme_check/checks/missing_enable_comment.rb +4 -0
  27. data/lib/theme_check/checks/missing_required_template_files.rb +5 -1
  28. data/lib/theme_check/checks/missing_template.rb +5 -1
  29. data/lib/theme_check/checks/translation_key_exists.rb +1 -0
  30. data/lib/theme_check/checks/undefined_object.rb +9 -1
  31. data/lib/theme_check/checks/unused_assign.rb +6 -1
  32. data/lib/theme_check/checks/unused_snippet.rb +50 -2
  33. data/lib/theme_check/config.rb +4 -3
  34. data/lib/theme_check/disabled_checks.rb +11 -4
  35. data/lib/theme_check/file_system_storage.rb +2 -0
  36. data/lib/theme_check/in_memory_storage.rb +1 -1
  37. data/lib/theme_check/json_printer.rb +1 -1
  38. data/lib/theme_check/language_server/bridge.rb +31 -6
  39. data/lib/theme_check/language_server/diagnostics_engine.rb +80 -34
  40. data/lib/theme_check/language_server/diagnostics_manager.rb +27 -6
  41. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +7 -6
  42. data/lib/theme_check/language_server/handler.rb +90 -8
  43. data/lib/theme_check/language_server/server.rb +42 -14
  44. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +17 -2
  45. data/lib/theme_check/liquid_file.rb +22 -1
  46. data/lib/theme_check/liquid_node.rb +33 -1
  47. data/lib/theme_check/liquid_visitor.rb +1 -1
  48. data/lib/theme_check/schema_helper.rb +1 -1
  49. data/lib/theme_check/shopify_liquid/object.rb +4 -0
  50. data/lib/theme_check/tags.rb +20 -3
  51. data/lib/theme_check/version.rb +1 -1
  52. data/theme-check.gemspec +2 -2
  53. metadata +12 -7
  54. data/.github/probots.yml +0 -3
@@ -28,7 +28,11 @@ module ThemeCheck
28
28
  private
29
29
 
30
30
  def ignore?(path)
31
- @ignore_missing.any? { |pattern| File.fnmatch?(pattern, path) }
31
+ all_ignored_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
32
+ end
33
+
34
+ def all_ignored_patterns
35
+ @all_ignored_patterns ||= @ignore_missing + ignored_patterns
32
36
  end
33
37
 
34
38
  def add_missing_offense(name, node:)
@@ -48,6 +48,7 @@ module ThemeCheck
48
48
 
49
49
  def key_exists?(key, pointer)
50
50
  key.split(".").each do |token|
51
+ return false unless pointer.is_a?(Hash)
51
52
  return false unless pointer.key?(token)
52
53
  pointer = pointer[token]
53
54
  end
@@ -55,7 +55,8 @@ module ThemeCheck
55
55
  end
56
56
  end
57
57
 
58
- def initialize(exclude_snippets: true)
58
+ def initialize(config_type: :default, exclude_snippets: true)
59
+ @config_type = config_type
59
60
  @exclude_snippets = exclude_snippets
60
61
  @files = {}
61
62
  end
@@ -111,6 +112,9 @@ module ThemeCheck
111
112
  shopify_plus_objects = ThemeCheck::ShopifyLiquid::Object.plus_labels
112
113
  shopify_plus_objects.freeze
113
114
 
115
+ theme_app_extension_objects = ThemeCheck::ShopifyLiquid::Object.theme_app_extension_labels
116
+ theme_app_extension_objects.freeze
117
+
114
118
  each_template do |(name, info)|
115
119
  if 'templates/customers/reset_password' == name
116
120
  # NOTE: `email` is exceptionally exposed as a theme object in
@@ -121,6 +125,8 @@ module ThemeCheck
121
125
  # the checkout template
122
126
  # https://shopify.dev/docs/themes/theme-templates/checkout-liquid#optional-objects
123
127
  check_object(info, all_global_objects + shopify_plus_objects)
128
+ elsif config_type == :theme_app_extension
129
+ check_object(info, all_global_objects + theme_app_extension_objects)
124
130
  else
125
131
  check_object(info, all_global_objects)
126
132
  end
@@ -129,6 +135,8 @@ module ThemeCheck
129
135
 
130
136
  private
131
137
 
138
+ attr_reader :config_type
139
+
132
140
  def ignore?(node)
133
141
  @exclude_snippets && node.theme_file.snippet?
134
142
  end
@@ -39,7 +39,12 @@ module ThemeCheck
39
39
  end
40
40
 
41
41
  def on_variable_lookup(node)
42
- @templates[node.theme_file.name].used_assigns << node.value.name
42
+ @templates[node.theme_file.name].used_assigns << case node.value.name
43
+ when Liquid::VariableLookup
44
+ node.value.name.name
45
+ else
46
+ node.value.name
47
+ end
43
48
  end
44
49
 
45
50
  def on_end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "set"
3
4
 
4
5
  module ThemeCheck
@@ -11,16 +12,22 @@ module ThemeCheck
11
12
  @used_snippets = Set.new
12
13
  end
13
14
 
14
- def on_include(node)
15
+ def on_render(node)
15
16
  if node.value.template_name_expr.is_a?(String)
16
17
  @used_snippets << "snippets/#{node.value.template_name_expr}"
18
+
19
+ elsif might_have_a_block_as_variable_lookup?(node)
20
+ # We ignore this case, because that's a "proper" use case for
21
+ # the render tag with OS 2.0
22
+ # {% render block %} shouldn't turn off the UnusedSnippet check
23
+
17
24
  else
18
25
  # Can't reliably track unused snippets if an expression is used, ignore this check
19
26
  @used_snippets.clear
20
27
  ignore!
21
28
  end
22
29
  end
23
- alias_method :on_render, :on_include
30
+ alias_method :on_include, :on_render
24
31
 
25
32
  def on_end
26
33
  missing_snippets.each do |theme_file|
@@ -33,5 +40,46 @@ module ThemeCheck
33
40
  def missing_snippets
34
41
  theme.snippets.reject { |t| @used_snippets.include?(t.name) }
35
42
  end
43
+
44
+ private
45
+
46
+ # This function returns true when the render node passed might have a
47
+ # variable lookup that refers to a block as template_name_expr.
48
+ #
49
+ # e.g.
50
+ #
51
+ # {% for block in col %}
52
+ # {% render block %}
53
+ # {% endfor %}
54
+ #
55
+ # In this case, the `block` variable_lookup in the render tag might be
56
+ # a Block because col might be an array of blocks.
57
+ #
58
+ # @param node [Node]
59
+ def might_have_a_block_as_variable_lookup?(node)
60
+ return false unless node.type_name == :render
61
+
62
+ return false unless node.value.template_name_expr.is_a?(Liquid::VariableLookup)
63
+
64
+ name = node.value.template_name_expr.name
65
+ return false unless name.is_a?(String)
66
+
67
+ # We're going through all the parents of the nodes until we find
68
+ # a For node with variable_name === to the template_name_expr's name
69
+ find_parent(node.parent) do |parent_node|
70
+ next false unless parent_node.type_name == :for
71
+
72
+ parent_node.value.variable_name == name
73
+ end
74
+ end
75
+
76
+ # @param node [Node]
77
+ def find_parent(node, &pred)
78
+ return nil unless node
79
+
80
+ return node if yield node
81
+
82
+ find_parent(node.parent, &pred)
83
+ end
36
84
  end
37
85
  end
@@ -126,14 +126,14 @@ module ThemeCheck
126
126
  options_for_check = options.transform_keys(&:to_sym)
127
127
  options_for_check.delete(:enabled)
128
128
  severity = options_for_check.delete(:severity)
129
- ignored_patterns = options_for_check.delete(:ignore) || []
129
+ check_ignored_patterns = options_for_check.delete(:ignore) || []
130
130
  check = if options_for_check.empty?
131
131
  check_class.new
132
132
  else
133
133
  check_class.new(**options_for_check)
134
134
  end
135
135
  check.severity = severity.to_sym if severity
136
- check.ignored_patterns = ignored_patterns
136
+ check.ignored_patterns = check_ignored_patterns + ignored_patterns
137
137
  check.options = options_for_check
138
138
  check
139
139
  end.compact
@@ -209,7 +209,8 @@ module ThemeCheck
209
209
 
210
210
  def resolve_requires
211
211
  self["require"]&.each do |path|
212
- require(File.join(@root, path))
212
+ file_to_require = @root.join(path).realpath
213
+ require(file_to_require.to_s)
213
214
  end
214
215
  end
215
216
  end
@@ -34,14 +34,16 @@ module ThemeCheck
34
34
  # We want to disable checks inside comments
35
35
  # (e.g. html checks inside {% comment %})
36
36
  disabled = @disabled_checks[[node.theme_file, :all]]
37
- disabled.start_index = node.inner_markup_start_index
38
- disabled.end_index = node.inner_markup_end_index
37
+ unless disabled.first_line
38
+ disabled.start_index = node.inner_markup_start_index
39
+ disabled.end_index = node.inner_markup_end_index
40
+ end
39
41
  end
40
42
  end
41
43
 
42
44
  def disabled?(check, theme_file, check_name, index)
43
45
  return true if check.ignored_patterns&.any? do |pattern|
44
- theme_file.relative_path.fnmatch?(pattern)
46
+ theme_file&.relative_path&.fnmatch?(pattern)
45
47
  end
46
48
 
47
49
  @disabled_checks[[theme_file, :all]]&.disabled?(index) ||
@@ -65,7 +67,12 @@ module ThemeCheck
65
67
  private
66
68
 
67
69
  def comment_text(node)
68
- node.value.nodelist.join
70
+ case node.type_name
71
+ when :comment
72
+ node.value.nodelist.join
73
+ when :inline_comment
74
+ node.markup.sub(/\s*#+\s*/, '')
75
+ end
69
76
  end
70
77
 
71
78
  def start_disabling?(text)
@@ -21,6 +21,8 @@ module ThemeCheck
21
21
 
22
22
  def read(relative_path)
23
23
  file(relative_path).read(mode: 'rb', encoding: 'UTF-8')
24
+ rescue Errno::ENOENT
25
+ nil
24
26
  end
25
27
 
26
28
  def write(relative_path, content)
@@ -9,7 +9,7 @@ module ThemeCheck
9
9
  attr_reader :root
10
10
 
11
11
  def initialize(files = {}, root = "/dev/null")
12
- @files = files
12
+ @files = files # Hash<String, String>
13
13
  @root = Pathname.new(root)
14
14
  end
15
15
 
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] },
26
26
  }
27
27
  end
28
- .sort_by { |o| o[:path] }
28
+ .sort_by { |o| o[:path] || Pathname.new('') }
29
29
  end
30
30
  end
31
31
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  # This class exists as a bridge (or boundary) between our handlers and the outside world.
4
6
  #
5
7
  # It is concerned with all the Language Server Protocol constructs. i.e.
@@ -29,6 +31,9 @@ module ThemeCheck
29
31
 
30
32
  # Whether the client supports work done progress notifications
31
33
  @supports_work_done_progress = false
34
+
35
+ @work_done_progress_mutex = Mutex.new
36
+ @work_done_progress_token = 1
32
37
  end
33
38
 
34
39
  def log(message)
@@ -78,12 +83,17 @@ module ThemeCheck
78
83
 
79
84
  # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
80
85
  def send_internal_error(id, e)
86
+ # For a reason I can't comprehend, sometimes
87
+ # e.full_message _hangs_ and brings your CPU to 100%.
88
+ # It's wrapped in here because it prints anyway...
89
+ # This shit is weird, yo.
90
+ Timeout.timeout(1) do
91
+ $stderr.puts e.full_message
92
+ end
93
+ ensure
81
94
  send_response(id, nil, {
82
95
  code: ErrorCodes::INTERNAL_ERROR,
83
- message: <<~EOS,
84
- #{e.class}: #{e.message}
85
- #{e.backtrace.join("\n ")}
86
- EOS
96
+ message: "A theme-check-language-server has occured, inspect OUTPUT logs for details.",
87
97
  })
88
98
  end
89
99
 
@@ -103,15 +113,28 @@ module ThemeCheck
103
113
  @supports_work_done_progress
104
114
  end
105
115
 
106
- def send_create_work_done_progress_request(token)
107
- return unless supports_work_done_progress?
116
+ def send_create_work_done_progress_request
117
+ # This isn't necessary, but it kind of is to make it obvious
118
+ # that this variable is not thread safe. Don't try to refactor
119
+ # this with @work_done_progress_token because you're going to
120
+ # have a hard time.
121
+ token = @work_done_progress_mutex.synchronize do
122
+ @work_done_progress_token += 1
123
+ end
124
+
125
+ return token unless supports_work_done_progress?
126
+
127
+ # We're going to wait for a response here...
108
128
  send_request("window/workDoneProgress/create", {
109
129
  token: token,
110
130
  })
131
+
132
+ token
111
133
  end
112
134
 
113
135
  def send_work_done_progress_begin(token, title)
114
136
  return unless supports_work_done_progress?
137
+
115
138
  send_progress(token, {
116
139
  kind: 'begin',
117
140
  title: title,
@@ -122,6 +145,7 @@ module ThemeCheck
122
145
 
123
146
  def send_work_done_progress_report(token, message, percentage)
124
147
  return unless supports_work_done_progress?
148
+
125
149
  send_progress(token, {
126
150
  kind: 'report',
127
151
  message: message,
@@ -132,6 +156,7 @@ module ThemeCheck
132
156
 
133
157
  def send_work_done_progress_end(token, message)
134
158
  return unless supports_work_done_progress?
159
+
135
160
  send_progress(token, {
136
161
  kind: 'end',
137
162
  message: message,
@@ -12,57 +12,103 @@ module ThemeCheck
12
12
  @diagnostics_manager = diagnostics_manager
13
13
  @storage = storage
14
14
  @bridge = bridge
15
- @token = 0
16
15
  end
17
16
 
18
17
  def first_run?
19
18
  @diagnostics_manager.first_run?
20
19
  end
21
20
 
22
- def analyze_and_send_offenses(absolute_path, config, force: false, only_single_file: false)
21
+ def analyze_and_send_offenses(absolute_path_or_paths, config, force: false, only_single_file: false)
23
22
  return unless @diagnostics_lock.try_lock
24
- @token += 1
25
- @bridge.send_create_work_done_progress_request(@token)
23
+
26
24
  theme = ThemeCheck::Theme.new(storage)
27
25
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
28
26
 
29
- if (!only_single_file && @diagnostics_manager.first_run?) || force
30
- @bridge.send_work_done_progress_begin(@token, "Full theme check")
31
- @bridge.log("Checking #{storage.root}")
32
- offenses = nil
33
- time = Benchmark.measure do
34
- offenses = analyzer.analyze_theme do |path, i, total|
35
- @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
36
- end
37
- end
38
- end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
39
- @bridge.send_work_done_progress_end(@token, end_message)
40
- @bridge.log(end_message)
41
- send_diagnostics(offenses)
27
+ if !only_single_file && (@diagnostics_manager.first_run? || force)
28
+ run_full_theme_check(analyzer)
42
29
  else
43
- # Analyze selected files
44
- relative_path = Pathname.new(storage.relative_path(absolute_path))
45
- file = theme[relative_path]
46
- # Skip if not a theme file
47
- if file
48
- @bridge.send_work_done_progress_begin(@token, "Partial theme check")
49
- offenses = nil
50
- time = Benchmark.measure do
51
- offenses = analyzer.analyze_files([file], only_single_file: only_single_file) do |path, i, total|
52
- @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
53
- end
54
- end
55
- end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
56
- @bridge.send_work_done_progress_end(@token, end_message)
57
- @bridge.log(end_message)
58
- send_diagnostics(offenses, [relative_path], only_single_file: only_single_file)
59
- end
30
+ run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
31
+ end
32
+
33
+ @diagnostics_lock.unlock
34
+ end
35
+
36
+ def clear_diagnostics(relative_paths)
37
+ return unless @diagnostics_lock.try_lock
38
+
39
+ as_array(relative_paths).each do |relative_path|
40
+ send_clearing_diagnostics(relative_path)
60
41
  end
42
+
61
43
  @diagnostics_lock.unlock
62
44
  end
63
45
 
64
46
  private
65
47
 
48
+ def run_full_theme_check(analyzer)
49
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
50
+
51
+ token = @bridge.send_create_work_done_progress_request
52
+ @bridge.send_work_done_progress_begin(token, "Full theme check")
53
+ @bridge.log("Checking #{storage.root}")
54
+ offenses = nil
55
+ time = Benchmark.measure do
56
+ offenses = analyzer.analyze_theme do |path, i, total|
57
+ @bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
58
+ end
59
+ end
60
+ end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
61
+ @bridge.send_work_done_progress_end(token, end_message)
62
+ @bridge.log(end_message)
63
+ send_diagnostics(offenses)
64
+ end
65
+
66
+ def run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
67
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
68
+
69
+ relative_paths = as_array(absolute_path_or_paths).map do |absolute_path|
70
+ Pathname.new(storage.relative_path(absolute_path))
71
+ end
72
+
73
+ theme_files = relative_paths
74
+ .map { |relative_path| theme[relative_path] }
75
+ .reject(&:nil?)
76
+
77
+ deleted_relative_paths = relative_paths - theme_files.map(&:relative_path)
78
+ deleted_relative_paths
79
+ .each { |p| send_clearing_diagnostics(p) }
80
+
81
+ token = @bridge.send_create_work_done_progress_request
82
+ @bridge.send_work_done_progress_begin(token, "Partial theme check")
83
+ offenses = nil
84
+ time = Benchmark.measure do
85
+ offenses = analyzer.analyze_files(theme_files, only_single_file: only_single_file) do |path, i, total|
86
+ @bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
87
+ end
88
+ end
89
+ end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
90
+ @bridge.send_work_done_progress_end(token, end_message)
91
+ @bridge.log(end_message)
92
+ send_diagnostics(offenses, theme_files.map(&:relative_path), only_single_file: only_single_file)
93
+ end
94
+
95
+ def send_clearing_diagnostics(relative_path)
96
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
97
+
98
+ relative_path = Pathname.new(relative_path) unless relative_path.is_a?(Pathname)
99
+ @diagnostics_manager.clear_diagnostics(relative_path)
100
+ send_diagnostic(relative_path, DiagnosticsManager::NO_DIAGNOSTICS)
101
+ end
102
+
103
+ def as_array(maybe_array)
104
+ case maybe_array
105
+ when Array
106
+ maybe_array
107
+ else
108
+ [maybe_array]
109
+ end
110
+ end
111
+
66
112
  def send_diagnostics(offenses, analyzed_files = nil, only_single_file: false)
67
113
  @diagnostics_manager.build_diagnostics(
68
114
  offenses,
@@ -4,6 +4,11 @@ require "logger"
4
4
  module ThemeCheck
5
5
  module LanguageServer
6
6
  class DiagnosticsManager
7
+ # The empty array is used in the protocol to mean that no
8
+ # diagnostics exist for this file. It's not always evident when
9
+ # reading code.
10
+ NO_DIAGNOSTICS = [].freeze
11
+
7
12
  # This class exists to facilitate LanguageServer diagnostics tracking.
8
13
  #
9
14
  # Motivations:
@@ -49,27 +54,27 @@ module ThemeCheck
49
54
  # When doing single file checks, we keep the whole theme old
50
55
  # ones and accept the new single ones
51
56
  if only_single_file && analyzed_paths.include?(path)
52
- single_file_diagnostics = current_diagnostics[path] || []
53
- whole_theme_diagnostics = whole_theme_diagnostics(path) || []
57
+ single_file_diagnostics = current_diagnostics[path] || NO_DIAGNOSTICS
58
+ whole_theme_diagnostics = whole_theme_diagnostics(path) || NO_DIAGNOSTICS
54
59
  [path, single_file_diagnostics + whole_theme_diagnostics]
55
60
 
56
61
  # If doing single file checks that are not in the
57
62
  # analyzed_paths array then we just keep the old
58
63
  # diagnostics
59
64
  elsif only_single_file
60
- [path, previous_diagnostics(path) || []]
65
+ [path, previous_diagnostics(path) || NO_DIAGNOSTICS]
61
66
 
62
67
  # When doing a full_check, we either send the current
63
68
  # diagnostics or an empty array to clear the diagnostics
64
69
  # for that file.
65
70
  elsif full_check
66
- [path, current_diagnostics[path] || []]
71
+ [path, current_diagnostics[path] || NO_DIAGNOSTICS]
67
72
 
68
73
  # When doing a partial check, the single file diagnostics
69
74
  # from the previous runs should be sent. Otherwise the
70
75
  # latest results are the good ones.
71
76
  else
72
- new_diagnostics = current_diagnostics[path] || []
77
+ new_diagnostics = current_diagnostics[path] || NO_DIAGNOSTICS
73
78
  should_use_cached_results = !analyzed_paths.include?(path)
74
79
  old_diagnostics = should_use_cached_results ? single_file_diagnostics(path) : []
75
80
  [path, new_diagnostics + old_diagnostics]
@@ -113,6 +118,13 @@ module ThemeCheck
113
118
  end.to_h
114
119
  end
115
120
 
121
+ # For when you know there shouldn't be anything on that file
122
+ # anymore. (e.g. file delete or file rename)
123
+ def clear_diagnostics(relative_path)
124
+ relative_path = sanitize_path(relative_path)
125
+ @latest_diagnostics.delete(relative_path)
126
+ end
127
+
116
128
  private
117
129
 
118
130
  def sanitize(diagnostics)
@@ -120,8 +132,17 @@ module ThemeCheck
120
132
  diagnostics
121
133
  end
122
134
 
135
+ def sanitize_path(relative_path)
136
+ case relative_path
137
+ when String
138
+ Pathname.new(relative_path)
139
+ else
140
+ relative_path
141
+ end
142
+ end
143
+
123
144
  def delete(relative_path, diagnostic)
124
- relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
145
+ relative_path = sanitize_path(relative_path)
125
146
  @mutex.synchronize do
126
147
  @latest_diagnostics[relative_path]&.delete(diagnostic)
127
148
  @latest_diagnostics.delete(relative_path) if @latest_diagnostics[relative_path]&.empty?
@@ -7,17 +7,18 @@ module ThemeCheck
7
7
 
8
8
  command "runChecks"
9
9
 
10
- def initialize(diagnostics_engine, root_path, root_config)
10
+ def initialize(diagnostics_engine, storage, linter_config, language_server_config)
11
11
  @diagnostics_engine = diagnostics_engine
12
- @root_path = root_path
13
- @root_config = root_config
12
+ @storage = storage
13
+ @linter_config = linter_config
14
+ @language_server_config = language_server_config
14
15
  end
15
16
 
16
17
  def execute(_args)
17
18
  @diagnostics_engine.analyze_and_send_offenses(
18
- @root_path,
19
- @root_config,
20
- only_single_file: false,
19
+ @storage.opened_files.map { |relative_path| @storage.path(relative_path) },
20
+ @linter_config,
21
+ only_single_file: @language_server_config.only_single_file?,
21
22
  force: true
22
23
  )
23
24
  nil