theme-check 1.7.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +49 -0
- data/README.md +10 -0
- data/RELEASING.md +13 -0
- data/config/default.yml +5 -0
- data/data/shopify_liquid/deprecated_filters.yml +4 -0
- data/data/shopify_liquid/filters.yml +3 -1
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/checks/TEMPLATE.md.erb +24 -19
- data/docs/checks/schema_json_format.md +76 -0
- data/docs/language_server/code-action-command-palette.png +0 -0
- data/docs/language_server/code-action-flow.png +0 -0
- data/docs/language_server/code-action-keyboard.png +0 -0
- data/docs/language_server/code-action-light-bulb.png +0 -0
- data/docs/language_server/code-action-problem.png +0 -0
- data/docs/language_server/code-action-quickfix.png +0 -0
- data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
- data/exe/theme-check-language-server +0 -4
- data/lib/theme_check/checks/asset_size_app_block_css.rb +2 -3
- data/lib/theme_check/checks/asset_size_app_block_javascript.rb +2 -3
- data/lib/theme_check/checks/asset_url_filters.rb +2 -0
- data/lib/theme_check/checks/default_locale.rb +1 -1
- data/lib/theme_check/checks/deprecated_filter.rb +79 -4
- data/lib/theme_check/checks/deprecated_global_app_block_type.rb +2 -3
- data/lib/theme_check/checks/matching_schema_translations.rb +14 -9
- data/lib/theme_check/checks/matching_translations.rb +1 -0
- data/lib/theme_check/checks/missing_required_template_files.rb +3 -3
- data/lib/theme_check/checks/missing_template.rb +1 -1
- data/lib/theme_check/checks/pagination_size.rb +2 -3
- data/lib/theme_check/checks/remote_asset.rb +5 -0
- data/lib/theme_check/checks/required_directories.rb +1 -1
- data/lib/theme_check/checks/required_layout_theme_object.rb +9 -4
- data/lib/theme_check/checks/schema_json_format.rb +29 -0
- data/lib/theme_check/checks/space_inside_braces.rb +132 -87
- data/lib/theme_check/checks/translation_key_exists.rb +33 -25
- data/lib/theme_check/checks/unused_assign.rb +3 -2
- data/lib/theme_check/checks/unused_snippet.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/checks/valid_schema.rb +2 -2
- data/lib/theme_check/corrector.rb +34 -23
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +8 -3
- data/lib/theme_check/html_node.rb +99 -6
- data/lib/theme_check/html_visitor.rb +1 -32
- data/lib/theme_check/in_memory_storage.rb +9 -0
- data/lib/theme_check/json_helpers.rb +14 -0
- data/lib/theme_check/language_server/bridge.rb +142 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/client_capabilities.rb +27 -0
- data/lib/theme_check/language_server/code_action_engine.rb +32 -0
- data/lib/theme_check/language_server/code_action_provider.rb +42 -0
- data/lib/theme_check/language_server/code_action_providers/quickfix_code_action_provider.rb +83 -0
- data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +40 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/configuration.rb +69 -0
- data/lib/theme_check/language_server/diagnostic.rb +124 -0
- data/lib/theme_check/language_server/diagnostics_engine.rb +80 -0
- data/lib/theme_check/language_server/diagnostics_manager.rb +136 -0
- data/lib/theme_check/language_server/document_change_corrector.rb +267 -0
- data/lib/theme_check/language_server/document_link_provider.rb +6 -6
- data/lib/theme_check/language_server/execute_command_engine.rb +19 -0
- data/lib/theme_check/language_server/execute_command_provider.rb +30 -0
- data/lib/theme_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
- data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +22 -0
- data/lib/theme_check/language_server/handler.rb +92 -217
- data/lib/theme_check/language_server/io_messenger.rb +112 -0
- data/lib/theme_check/language_server/messenger.rb +12 -42
- data/lib/theme_check/language_server/protocol.rb +4 -0
- data/lib/theme_check/language_server/server.rb +54 -110
- data/lib/theme_check/language_server/uri_helper.rb +1 -0
- data/lib/theme_check/language_server/versioned_in_memory_storage.rb +69 -0
- data/lib/theme_check/language_server.rb +28 -6
- data/lib/theme_check/liquid_node.rb +255 -12
- data/lib/theme_check/locale_diff.rb +48 -10
- data/lib/theme_check/node.rb +16 -0
- data/lib/theme_check/offense.rb +27 -23
- data/lib/theme_check/position.rb +4 -4
- data/lib/theme_check/regex_helpers.rb +1 -1
- data/lib/theme_check/schema_helper.rb +70 -0
- data/lib/theme_check/shopify_liquid/system_translations.rb +35 -0
- data/lib/theme_check/shopify_liquid/tag.rb +19 -1
- data/lib/theme_check/shopify_liquid.rb +1 -0
- data/lib/theme_check/storage.rb +4 -0
- data/lib/theme_check/tags.rb +0 -1
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/theme_file.rb +8 -1
- data/lib/theme_check/theme_file_rewriter.rb +28 -6
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -2
- metadata +31 -3
- data/lib/theme_check/language_server/diagnostics_tracker.rb +0 -66
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class CodeActionProvider
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
@all ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def inherited(subclass)
|
12
|
+
all << subclass
|
13
|
+
end
|
14
|
+
|
15
|
+
def kind(k = nil)
|
16
|
+
@kind = k unless k.nil?
|
17
|
+
@kind
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :storage
|
22
|
+
attr_reader :diagnostics_manager
|
23
|
+
|
24
|
+
def initialize(storage, diagnostics_manager)
|
25
|
+
@storage = storage
|
26
|
+
@diagnostics_manager = diagnostics_manager
|
27
|
+
end
|
28
|
+
|
29
|
+
def kind
|
30
|
+
self.class.kind
|
31
|
+
end
|
32
|
+
|
33
|
+
def base_kind
|
34
|
+
kind.split('.')[0]
|
35
|
+
end
|
36
|
+
|
37
|
+
def code_actions(relative_path, range)
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class QuickfixCodeActionProvider < CodeActionProvider
|
6
|
+
kind "quickfix"
|
7
|
+
|
8
|
+
def code_actions(relative_path, range)
|
9
|
+
correctable_diagnostics = diagnostics_manager
|
10
|
+
.diagnostics(relative_path)
|
11
|
+
.filter(&:correctable?)
|
12
|
+
.reject do |diagnostic|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
diagnostics_under_cursor = correctable_diagnostics
|
19
|
+
.filter { |diagnostic| diagnostic.offense.in_range?(range) }
|
20
|
+
|
21
|
+
return [] if diagnostics_under_cursor.empty?
|
22
|
+
|
23
|
+
(
|
24
|
+
quickfix_cursor_code_actions(diagnostics_under_cursor) +
|
25
|
+
quickfix_all_of_type_code_actions(diagnostics_under_cursor, correctable_diagnostics) +
|
26
|
+
quickfix_all_code_action(correctable_diagnostics)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def quickfix_cursor_code_actions(diagnostics)
|
33
|
+
diagnostics.map do |diagnostic|
|
34
|
+
{
|
35
|
+
title: "Fix this #{diagnostic.code} problem: #{diagnostic.message}",
|
36
|
+
kind: kind,
|
37
|
+
diagnostics: [diagnostic.to_h],
|
38
|
+
isPreferred: true,
|
39
|
+
command: {
|
40
|
+
title: 'quickfix',
|
41
|
+
command: CorrectionExecuteCommandProvider.command,
|
42
|
+
arguments: [diagnostic.to_h],
|
43
|
+
},
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def quickfix_all_of_type_code_actions(cursor_diagnostics, correctable_diagnostics)
|
49
|
+
codes = Set.new(cursor_diagnostics.map(&:code))
|
50
|
+
correctable_diagnostics_by_code = correctable_diagnostics.group_by(&:code)
|
51
|
+
codes.flat_map do |code|
|
52
|
+
diagnostics = correctable_diagnostics_by_code[code].map(&:to_h)
|
53
|
+
return [] unless diagnostics.size > 1
|
54
|
+
{
|
55
|
+
title: "Fix all #{code} problems",
|
56
|
+
kind: kind,
|
57
|
+
diagnostics: diagnostics,
|
58
|
+
command: {
|
59
|
+
title: 'quickfix',
|
60
|
+
command: CorrectionExecuteCommandProvider.command,
|
61
|
+
arguments: diagnostics,
|
62
|
+
},
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def quickfix_all_code_action(diagnostics)
|
68
|
+
return [] unless diagnostics.size > 1
|
69
|
+
diagnostics = diagnostics.map(&:to_h)
|
70
|
+
[{
|
71
|
+
title: "Fix all auto-fixable problems",
|
72
|
+
kind: kind,
|
73
|
+
diagnostics: diagnostics,
|
74
|
+
command: {
|
75
|
+
title: 'quickfix',
|
76
|
+
command: CorrectionExecuteCommandProvider.command,
|
77
|
+
arguments: diagnostics,
|
78
|
+
},
|
79
|
+
}]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/theme_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class SourceFixAllCodeActionProvider < CodeActionProvider
|
6
|
+
kind "source.fixAll"
|
7
|
+
|
8
|
+
def code_actions(relative_path, _)
|
9
|
+
diagnostics = diagnostics_manager
|
10
|
+
.diagnostics(relative_path)
|
11
|
+
.filter(&:correctable?)
|
12
|
+
.reject do |diagnostic|
|
13
|
+
# We cannot quickfix if the buffer was modified. This means
|
14
|
+
# our diagnostics and InMemoryStorage are out of sync.
|
15
|
+
diagnostic.file_version != storage.version(diagnostic.relative_path)
|
16
|
+
end
|
17
|
+
.map(&:to_h)
|
18
|
+
diagnostics_to_code_action(diagnostics)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def diagnostics_to_code_action(diagnostics)
|
24
|
+
return [] if diagnostics.empty?
|
25
|
+
[
|
26
|
+
{
|
27
|
+
title: "Fix all Theme Check auto-fixable problems",
|
28
|
+
kind: kind,
|
29
|
+
diagnostics: diagnostics,
|
30
|
+
command: {
|
31
|
+
title: 'fixAll.file',
|
32
|
+
command: LanguageServer::CorrectionExecuteCommandProvider.command,
|
33
|
+
arguments: diagnostics,
|
34
|
+
},
|
35
|
+
},
|
36
|
+
]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -6,7 +6,9 @@ module ThemeCheck
|
|
6
6
|
def completions(content, cursor)
|
7
7
|
return [] unless can_complete?(content, cursor)
|
8
8
|
partial = first_word(content) || ''
|
9
|
-
ShopifyLiquid::Tag.labels
|
9
|
+
labels = ShopifyLiquid::Tag.labels
|
10
|
+
labels += ShopifyLiquid::Tag.end_labels
|
11
|
+
labels
|
10
12
|
.select { |w| w.start_with?(partial) }
|
11
13
|
.map { |tag| tag_to_completion(tag) }
|
12
14
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class Configuration
|
6
|
+
CHECK_ON_OPEN = :"themeCheck.checkOnOpen"
|
7
|
+
CHECK_ON_SAVE = :"themeCheck.checkOnSave"
|
8
|
+
CHECK_ON_CHANGE = :"themeCheck.checkOnChange"
|
9
|
+
|
10
|
+
def initialize(bridge, capabilities)
|
11
|
+
@bridge = bridge
|
12
|
+
@capabilities = capabilities
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@initialized = false
|
15
|
+
@config = {
|
16
|
+
CHECK_ON_OPEN => @capabilities.initialization_option(CHECK_ON_OPEN) || true,
|
17
|
+
CHECK_ON_SAVE => @capabilities.initialization_option(CHECK_ON_SAVE) || true,
|
18
|
+
CHECK_ON_CHANGE => @capabilities.initialization_option(CHECK_ON_CHANGE) || true,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(force: nil)
|
23
|
+
@mutex.synchronize do
|
24
|
+
return unless @capabilities.supports_workspace_configuration?
|
25
|
+
return if initialized? && !force
|
26
|
+
check_on_open, check_on_save, check_on_change = @bridge.send_request(
|
27
|
+
"workspace/configuration",
|
28
|
+
items: [
|
29
|
+
{ section: CHECK_ON_OPEN },
|
30
|
+
{ section: CHECK_ON_SAVE },
|
31
|
+
{ section: CHECK_ON_CHANGE },
|
32
|
+
],
|
33
|
+
)
|
34
|
+
@config[CHECK_ON_OPEN] = check_on_open unless check_on_open.nil?
|
35
|
+
@config[CHECK_ON_CHANGE] = check_on_change unless check_on_change.nil?
|
36
|
+
@config[CHECK_ON_SAVE] = check_on_save unless check_on_save.nil?
|
37
|
+
@initialized = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def register_did_change_capability
|
42
|
+
return unless @capabilities.supports_workspace_did_change_configuration_dynamic_registration?
|
43
|
+
@bridge.send_request('client/registerCapability', registrations: [{
|
44
|
+
id: "workspace/didChangeConfiguration",
|
45
|
+
method: "workspace/didChangeConfiguration",
|
46
|
+
}])
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialized?
|
50
|
+
@initialized
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_on_open?
|
54
|
+
fetch # making sure we have an initialized value
|
55
|
+
@config[CHECK_ON_OPEN]
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_on_save?
|
59
|
+
fetch # making sure we have for an initialized value
|
60
|
+
@config[CHECK_ON_SAVE]
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_on_change?
|
64
|
+
fetch # making sure we have for an initialized value
|
65
|
+
@config[CHECK_ON_CHANGE]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class Diagnostic
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
attr_reader :offense
|
9
|
+
|
10
|
+
def initialize(offense)
|
11
|
+
@offense = offense
|
12
|
+
@diagnostic = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
case other
|
17
|
+
when Hash, Diagnostic
|
18
|
+
to_h == other.to_h
|
19
|
+
else
|
20
|
+
raise ArgumentError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
return @diagnostic unless @diagnostic.nil?
|
26
|
+
@diagnostic = {
|
27
|
+
source: "theme-check",
|
28
|
+
code: code,
|
29
|
+
message: message,
|
30
|
+
range: range,
|
31
|
+
severity: severity,
|
32
|
+
data: data,
|
33
|
+
}
|
34
|
+
@diagnostic[:codeDescription] = code_description unless offense.doc.nil?
|
35
|
+
@diagnostic
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
to_h.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def single_file?
|
43
|
+
offense.single_file?
|
44
|
+
end
|
45
|
+
|
46
|
+
def correctable?
|
47
|
+
offense.correctable?
|
48
|
+
end
|
49
|
+
|
50
|
+
def code
|
51
|
+
offense.code_name
|
52
|
+
end
|
53
|
+
|
54
|
+
def message
|
55
|
+
offense.message
|
56
|
+
end
|
57
|
+
|
58
|
+
def code_description
|
59
|
+
{
|
60
|
+
href: offense.doc,
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def severity
|
65
|
+
case offense.severity
|
66
|
+
when :error
|
67
|
+
1
|
68
|
+
when :suggestion
|
69
|
+
2
|
70
|
+
when :style
|
71
|
+
3
|
72
|
+
else
|
73
|
+
4
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def range
|
78
|
+
{
|
79
|
+
start: {
|
80
|
+
line: offense.start_row,
|
81
|
+
character: offense.start_column,
|
82
|
+
},
|
83
|
+
end: {
|
84
|
+
line: offense.end_row,
|
85
|
+
character: offense.end_column,
|
86
|
+
},
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def start_index
|
91
|
+
offense.start_index
|
92
|
+
end
|
93
|
+
|
94
|
+
def end_index
|
95
|
+
offense.end_index
|
96
|
+
end
|
97
|
+
|
98
|
+
def absolute_path
|
99
|
+
@absolute_path ||= offense&.theme_file&.path
|
100
|
+
end
|
101
|
+
|
102
|
+
def relative_path
|
103
|
+
@relative_path ||= offense&.theme_file&.relative_path
|
104
|
+
end
|
105
|
+
|
106
|
+
def uri
|
107
|
+
@uri ||= absolute_path && file_uri(absolute_path)
|
108
|
+
end
|
109
|
+
|
110
|
+
def file_version
|
111
|
+
@version ||= offense&.version
|
112
|
+
end
|
113
|
+
|
114
|
+
def data
|
115
|
+
{
|
116
|
+
absolute_path: absolute_path.to_s,
|
117
|
+
relative_path: relative_path.to_s,
|
118
|
+
uri: uri,
|
119
|
+
version: file_version,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class DiagnosticsEngine
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
attr_reader :storage
|
9
|
+
|
10
|
+
def initialize(storage, bridge, diagnostics_manager = DiagnosticsManager.new)
|
11
|
+
@diagnostics_lock = Mutex.new
|
12
|
+
@diagnostics_manager = diagnostics_manager
|
13
|
+
@storage = storage
|
14
|
+
@bridge = bridge
|
15
|
+
@token = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def first_run?
|
19
|
+
@diagnostics_manager.first_run?
|
20
|
+
end
|
21
|
+
|
22
|
+
def analyze_and_send_offenses(absolute_path, config, force: false)
|
23
|
+
return unless @diagnostics_lock.try_lock
|
24
|
+
@token += 1
|
25
|
+
@bridge.send_create_work_done_progress_request(@token)
|
26
|
+
theme = ThemeCheck::Theme.new(storage)
|
27
|
+
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
28
|
+
|
29
|
+
if @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
|
+
send_diagnostics(offenses)
|
41
|
+
else
|
42
|
+
# Analyze selected files
|
43
|
+
relative_path = Pathname.new(storage.relative_path(absolute_path))
|
44
|
+
file = theme[relative_path]
|
45
|
+
# Skip if not a theme file
|
46
|
+
if file
|
47
|
+
@bridge.send_work_done_progress_begin(@token, "Partial theme check")
|
48
|
+
offenses = nil
|
49
|
+
time = Benchmark.measure do
|
50
|
+
offenses = analyzer.analyze_files([file]) do |path, i, total|
|
51
|
+
@bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
|
55
|
+
@bridge.send_work_done_progress_end(@token, end_message)
|
56
|
+
@bridge.log(end_message)
|
57
|
+
send_diagnostics(offenses, [relative_path])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
@diagnostics_lock.unlock
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def send_diagnostics(offenses, analyzed_files = nil)
|
66
|
+
@diagnostics_manager.build_diagnostics(offenses, analyzed_files: analyzed_files).each do |relative_path, diagnostics|
|
67
|
+
send_diagnostic(relative_path, diagnostics)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def send_diagnostic(relative_path, diagnostics)
|
72
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
73
|
+
@bridge.send_notification('textDocument/publishDiagnostics', {
|
74
|
+
uri: file_uri(storage.path(relative_path)),
|
75
|
+
diagnostics: diagnostics.map(&:to_h),
|
76
|
+
})
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
module LanguageServer
|
6
|
+
class DiagnosticsManager
|
7
|
+
# This class exists to facilitate LanguageServer diagnostics tracking.
|
8
|
+
#
|
9
|
+
# Motivations:
|
10
|
+
# 1. The first time we lint, we want all the errors from all the files.
|
11
|
+
# 2. If we fix all the errors in a file, we have to send an empty array for that file.
|
12
|
+
# 3. If we do a partial check, we should consider the whole theme diagnostics as valid, and return cached results
|
13
|
+
# 4. We should be able to create WorkspaceEdits from diagnostics, so that the ExecuteCommandEngine can do its job
|
14
|
+
# 5. We should clean up diagnostics that were applied by the client
|
15
|
+
def initialize
|
16
|
+
@latest_diagnostics = {} # { [Pathname(relative_path)] => Diagnostic[] }
|
17
|
+
@mutex = Mutex.new
|
18
|
+
@first_run = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def first_run?
|
22
|
+
@first_run
|
23
|
+
end
|
24
|
+
|
25
|
+
def diagnostics(relative_path)
|
26
|
+
relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
|
27
|
+
@mutex.synchronize { @latest_diagnostics[relative_path] || [] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_diagnostics(offenses, analyzed_files: nil)
|
31
|
+
@mutex.synchronize do
|
32
|
+
full_check = analyzed_files.nil?
|
33
|
+
analyzed_paths = analyzed_files.map { |path| Pathname.new(path) } unless full_check
|
34
|
+
|
35
|
+
# When analyzed_files is nil, contains all offenses.
|
36
|
+
# When analyzed_files is !nil, contains all whole theme offenses and single file offenses of the analyzed_files.
|
37
|
+
current_diagnostics = offenses
|
38
|
+
.select(&:theme_file)
|
39
|
+
.group_by(&:theme_file)
|
40
|
+
.transform_keys { |theme_file| Pathname.new(theme_file.relative_path) }
|
41
|
+
.transform_values do |theme_file_offenses|
|
42
|
+
theme_file_offenses.map { |o| Diagnostic.new(o) }
|
43
|
+
end
|
44
|
+
|
45
|
+
previous_paths = paths(@latest_diagnostics)
|
46
|
+
current_paths = paths(current_diagnostics)
|
47
|
+
|
48
|
+
diagnostics_update = (current_paths + previous_paths).map do |path|
|
49
|
+
# When doing a full_check, we either send the current
|
50
|
+
# diagnostics or an empty array to clear the diagnostics
|
51
|
+
# for that file.
|
52
|
+
if full_check
|
53
|
+
[path, current_diagnostics[path] || []]
|
54
|
+
|
55
|
+
# When doing a partial check, the single file diagnostics
|
56
|
+
# from the previous runs should be sent. Otherwise the
|
57
|
+
# latest results are the good ones.
|
58
|
+
else
|
59
|
+
new_diagnostics = current_diagnostics[path] || []
|
60
|
+
should_use_cached_results = !analyzed_paths.include?(path)
|
61
|
+
old_diagnostics = should_use_cached_results ? single_file_diagnostics(path) : []
|
62
|
+
[path, new_diagnostics + old_diagnostics]
|
63
|
+
end
|
64
|
+
end.to_h
|
65
|
+
|
66
|
+
@latest_diagnostics = diagnostics_update.reject { |_, v| v.empty? }
|
67
|
+
@first_run = false
|
68
|
+
diagnostics_update
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def workspace_edit(diagnostics)
|
73
|
+
diagnostics = sanitize(diagnostics)
|
74
|
+
.select(&:correctable?)
|
75
|
+
|
76
|
+
{
|
77
|
+
documentChanges: document_changes(diagnostics),
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete_applied(diagnostics)
|
82
|
+
diagnostics = sanitize(diagnostics)
|
83
|
+
.select(&:correctable?)
|
84
|
+
|
85
|
+
previous_paths = paths(@latest_diagnostics)
|
86
|
+
|
87
|
+
diagnostics.each do |diagnostic|
|
88
|
+
delete(diagnostic.relative_path, diagnostic)
|
89
|
+
end
|
90
|
+
|
91
|
+
current_paths = paths(@latest_diagnostics)
|
92
|
+
|
93
|
+
(current_paths + previous_paths).map do |path|
|
94
|
+
[path, @latest_diagnostics[path] || []]
|
95
|
+
end.to_h
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def sanitize(diagnostics)
|
101
|
+
diagnostics = diagnostics.map { |hash| find(hash) }.reject(&:nil?) if diagnostics[0]&.is_a?(Hash)
|
102
|
+
diagnostics
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete(relative_path, diagnostic)
|
106
|
+
relative_path = Pathname.new(relative_path) if relative_path.is_a?(String)
|
107
|
+
@mutex.synchronize do
|
108
|
+
@latest_diagnostics[relative_path]&.delete(diagnostic)
|
109
|
+
@latest_diagnostics.delete(relative_path) if @latest_diagnostics[relative_path]&.empty?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def find(diagnostic_hash)
|
114
|
+
diagnostics(diagnostic_hash.dig(:data, :relative_path))
|
115
|
+
.find { |d| d == diagnostic_hash }
|
116
|
+
end
|
117
|
+
|
118
|
+
def document_changes(diagnostics)
|
119
|
+
corrector = DocumentChangeCorrector.new
|
120
|
+
diagnostics.each do |diagnostic|
|
121
|
+
offense = diagnostic.offense
|
122
|
+
offense.correct(corrector)
|
123
|
+
end
|
124
|
+
corrector.document_changes
|
125
|
+
end
|
126
|
+
|
127
|
+
def paths(diagnostics)
|
128
|
+
(diagnostics || {}).keys.map { |path| Pathname.new(path) }.to_set
|
129
|
+
end
|
130
|
+
|
131
|
+
def single_file_diagnostics(relative_path)
|
132
|
+
@latest_diagnostics[relative_path]&.select(&:single_file?) || []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|