theme-check 1.10.1 → 1.11.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 (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