theme-check 1.10.3 → 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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/theme-check.yml +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +7 -8
- data/config/default.yml +4 -0
- data/data/shopify_liquid/filters.yml +18 -0
- data/dev.yml +1 -1
- data/docs/checks/asset_preload.md +60 -0
- data/docs/checks/asset_size_javascript.md +2 -2
- data/docs/checks/missing_enable_comment.md +3 -3
- data/docs/checks/nested_snippet.md +8 -8
- data/docs/checks/translation_key_exists.md +4 -4
- data/docs/checks/valid_html_translation.md +1 -1
- data/lib/theme_check/analyzer.rb +18 -3
- data/lib/theme_check/check.rb +6 -1
- data/lib/theme_check/checks/asset_preload.rb +20 -0
- data/lib/theme_check/checks/deprecated_filter.rb +29 -5
- data/lib/theme_check/checks/missing_enable_comment.rb +4 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +5 -1
- data/lib/theme_check/checks/missing_template.rb +5 -1
- data/lib/theme_check/checks/unused_assign.rb +6 -1
- data/lib/theme_check/checks/unused_snippet.rb +50 -2
- data/lib/theme_check/config.rb +2 -2
- data/lib/theme_check/disabled_checks.rb +11 -4
- data/lib/theme_check/file_system_storage.rb +2 -0
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/language_server/bridge.rb +31 -6
- data/lib/theme_check/language_server/diagnostics_engine.rb +80 -34
- data/lib/theme_check/language_server/diagnostics_manager.rb +27 -6
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +7 -6
- data/lib/theme_check/language_server/handler.rb +90 -8
- data/lib/theme_check/language_server/server.rb +42 -14
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +17 -2
- data/lib/theme_check/liquid_file.rb +22 -1
- data/lib/theme_check/liquid_node.rb +33 -1
- data/lib/theme_check/liquid_visitor.rb +1 -1
- data/lib/theme_check/schema_helper.rb +1 -1
- data/lib/theme_check/tags.rb +2 -1
- data/lib/theme_check/version.rb +1 -1
- data/theme-check.gemspec +2 -2
- metadata +10 -6
- data/.github/probots.yml +0 -3
@@ -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
|
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 :
|
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
|
data/lib/theme_check/config.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -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.
|
38
|
-
|
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
|
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.
|
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)
|
@@ -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:
|
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
|
107
|
-
|
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(
|
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
|
-
|
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
|
30
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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 =
|
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,
|
10
|
+
def initialize(diagnostics_engine, storage, linter_config, language_server_config)
|
11
11
|
@diagnostics_engine = diagnostics_engine
|
12
|
-
@
|
13
|
-
@
|
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
|
-
@
|
19
|
-
@
|
20
|
-
only_single_file:
|
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
|
@@ -12,6 +12,16 @@ module ThemeCheck
|
|
12
12
|
version: ThemeCheck::VERSION,
|
13
13
|
}
|
14
14
|
|
15
|
+
# https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileOperationFilter
|
16
|
+
FILE_OPERATION_FILTER = {
|
17
|
+
filters: [{
|
18
|
+
scheme: 'file',
|
19
|
+
pattern: {
|
20
|
+
glob: '**/*',
|
21
|
+
},
|
22
|
+
}],
|
23
|
+
}
|
24
|
+
|
15
25
|
CAPABILITIES = {
|
16
26
|
completionProvider: {
|
17
27
|
triggerCharacters: ['.', '{{ ', '{% '],
|
@@ -33,6 +43,13 @@ module ThemeCheck
|
|
33
43
|
willSave: false,
|
34
44
|
save: true,
|
35
45
|
},
|
46
|
+
workspace: {
|
47
|
+
fileOperations: {
|
48
|
+
didCreate: FILE_OPERATION_FILTER,
|
49
|
+
didDelete: FILE_OPERATION_FILTER,
|
50
|
+
willRename: FILE_OPERATION_FILTER,
|
51
|
+
},
|
52
|
+
},
|
36
53
|
}
|
37
54
|
|
38
55
|
def initialize(bridge)
|
@@ -55,7 +72,12 @@ module ThemeCheck
|
|
55
72
|
@diagnostics_engine = DiagnosticsEngine.new(@storage, @bridge, @diagnostics_manager)
|
56
73
|
@execute_command_engine = ExecuteCommandEngine.new
|
57
74
|
@execute_command_engine << CorrectionExecuteCommandProvider.new(@storage, @bridge, @diagnostics_manager)
|
58
|
-
@execute_command_engine << RunChecksExecuteCommandProvider.new(
|
75
|
+
@execute_command_engine << RunChecksExecuteCommandProvider.new(
|
76
|
+
@diagnostics_engine,
|
77
|
+
@storage,
|
78
|
+
config_for_path(@root_path),
|
79
|
+
@configuration,
|
80
|
+
)
|
59
81
|
@code_action_engine = CodeActionEngine.new(@storage, @diagnostics_manager)
|
60
82
|
@bridge.send_response(id, {
|
61
83
|
capabilities: CAPABILITIES,
|
@@ -65,6 +87,7 @@ module ThemeCheck
|
|
65
87
|
|
66
88
|
def on_initialized(_id, _params)
|
67
89
|
return unless @configuration
|
90
|
+
|
68
91
|
@configuration.fetch
|
69
92
|
@configuration.register_did_change_capability
|
70
93
|
end
|
@@ -91,9 +114,17 @@ module ThemeCheck
|
|
91
114
|
|
92
115
|
def on_text_document_did_close(_id, params)
|
93
116
|
relative_path = relative_path_from_text_document_uri(params)
|
94
|
-
|
95
|
-
|
96
|
-
|
117
|
+
begin
|
118
|
+
file_system_content = Pathname.new(text_document_uri(params)).read(mode: 'rb', encoding: 'UTF-8')
|
119
|
+
# On close, the file system becomes the source of truth
|
120
|
+
@storage.write(relative_path, file_system_content, nil)
|
121
|
+
|
122
|
+
# the file no longer exists because either the user deleted it, or the user renamed it.
|
123
|
+
rescue Errno::ENOENT
|
124
|
+
@storage.remove(relative_path)
|
125
|
+
ensure
|
126
|
+
@diagnostics_engine.clear_diagnostics(relative_path) if @configuration.only_single_file?
|
127
|
+
end
|
97
128
|
end
|
98
129
|
|
99
130
|
def on_text_document_did_save(_id, params)
|
@@ -125,6 +156,52 @@ module ThemeCheck
|
|
125
156
|
))
|
126
157
|
end
|
127
158
|
|
159
|
+
def on_workspace_did_create_files(_id, params)
|
160
|
+
paths = params[:files]
|
161
|
+
&.map { |file| file[:uri] }
|
162
|
+
&.map { |uri| file_path(uri) }
|
163
|
+
return unless paths
|
164
|
+
|
165
|
+
paths.each do |path|
|
166
|
+
relative_path = @storage.relative_path(path)
|
167
|
+
file_system_content = Pathname.new(path).read(mode: 'rb', encoding: 'UTF-8')
|
168
|
+
@storage.write(relative_path, file_system_content, nil)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def on_workspace_did_delete_files(_id, params)
|
173
|
+
absolute_paths = params[:files]
|
174
|
+
&.map { |file| file[:uri] }
|
175
|
+
&.map { |uri| file_path(uri) }
|
176
|
+
return unless absolute_paths
|
177
|
+
|
178
|
+
absolute_paths.each do |path|
|
179
|
+
relative_path = @storage.relative_path(path)
|
180
|
+
@storage.remove(relative_path)
|
181
|
+
end
|
182
|
+
|
183
|
+
analyze_and_send_offenses(absolute_paths)
|
184
|
+
end
|
185
|
+
|
186
|
+
# We're using workspace/willRenameFiles here because we want this to run
|
187
|
+
# before textDocument/didOpen and textDocumetn/didClose of the files
|
188
|
+
# (which might trigger another theme analysis).
|
189
|
+
def on_workspace_will_rename_files(id, params)
|
190
|
+
relative_paths = params[:files]
|
191
|
+
&.map { |file| [file[:oldUri], file[:newUri]] }
|
192
|
+
&.map { |(old_uri, new_uri)| [relative_path_from_uri(old_uri), relative_path_from_uri(new_uri)] }
|
193
|
+
return @bridge.send_response(id, nil) unless relative_paths
|
194
|
+
|
195
|
+
relative_paths.each do |(old_path, new_path)|
|
196
|
+
@storage.write(new_path, @storage.read(old_path), nil)
|
197
|
+
@storage.remove(old_path)
|
198
|
+
end
|
199
|
+
@bridge.send_response(id, nil)
|
200
|
+
|
201
|
+
absolute_paths = relative_paths.flatten(2).map { |p| @storage.path(p) }
|
202
|
+
analyze_and_send_offenses(absolute_paths)
|
203
|
+
end
|
204
|
+
|
128
205
|
def on_workspace_execute_command(id, params)
|
129
206
|
@bridge.send_response(id, @execute_command_engine.execute(
|
130
207
|
params[:command],
|
@@ -159,6 +236,10 @@ module ThemeCheck
|
|
159
236
|
file_path(params.dig(:textDocument, :uri))
|
160
237
|
end
|
161
238
|
|
239
|
+
def relative_path_from_uri(uri)
|
240
|
+
@storage.relative_path(file_path(uri))
|
241
|
+
end
|
242
|
+
|
162
243
|
def relative_path_from_text_document_uri(params)
|
163
244
|
@storage.relative_path(text_document_uri(params))
|
164
245
|
end
|
@@ -185,15 +266,16 @@ module ThemeCheck
|
|
185
266
|
params.dig(:contentChanges, 0, :text)
|
186
267
|
end
|
187
268
|
|
188
|
-
def config_for_path(
|
269
|
+
def config_for_path(path_or_paths)
|
270
|
+
path = path_or_paths.is_a?(Array) ? path_or_paths[0] : path_or_paths
|
189
271
|
root = ThemeCheck::Config.find(path) || @root_path
|
190
272
|
ThemeCheck::Config.from_path(root)
|
191
273
|
end
|
192
274
|
|
193
|
-
def analyze_and_send_offenses(
|
275
|
+
def analyze_and_send_offenses(absolute_path_or_paths, only_single_file: nil)
|
194
276
|
@diagnostics_engine.analyze_and_send_offenses(
|
195
|
-
|
196
|
-
config_for_path(
|
277
|
+
absolute_path_or_paths,
|
278
|
+
config_for_path(absolute_path_or_paths),
|
197
279
|
only_single_file: only_single_file.nil? ? @configuration.only_single_file? : only_single_file
|
198
280
|
)
|
199
281
|
end
|