theme-check 1.6.0 → 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/data/shopify_liquid/tags.yml +9 -9
- data/docs/api/html_check.md +7 -7
- data/docs/api/liquid_check.md +10 -10
- data/docs/checks/convert_include_to_render.md +1 -1
- data/docs/checks/missing_enable_comment.md +1 -1
- data/lib/theme_check/analyzer.rb +41 -17
- data/lib/theme_check/asset_file.rb +1 -1
- data/lib/theme_check/check.rb +2 -2
- data/lib/theme_check/checks/html_parsing_error.rb +2 -2
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/missing_template.rb +6 -6
- data/lib/theme_check/checks/nested_snippet.rb +2 -2
- data/lib/theme_check/checks/required_layout_theme_object.rb +2 -2
- data/lib/theme_check/checks/syntax_error.rb +5 -5
- data/lib/theme_check/checks/template_length.rb +2 -2
- data/lib/theme_check/checks/translation_key_exists.rb +1 -13
- data/lib/theme_check/checks/undefined_object.rb +7 -7
- data/lib/theme_check/checks/unused_assign.rb +4 -4
- data/lib/theme_check/checks/unused_snippet.rb +7 -7
- data/lib/theme_check/checks/valid_json.rb +1 -1
- data/lib/theme_check/checks.rb +4 -2
- data/lib/theme_check/cli.rb +1 -1
- data/lib/theme_check/corrector.rb +6 -6
- data/lib/theme_check/disabled_check.rb +3 -3
- data/lib/theme_check/disabled_checks.rb +9 -9
- data/lib/theme_check/exceptions.rb +1 -0
- data/lib/theme_check/file_system_storage.rb +4 -0
- data/lib/theme_check/html_node.rb +36 -28
- data/lib/theme_check/html_visitor.rb +6 -6
- data/lib/theme_check/in_memory_storage.rb +1 -1
- data/lib/theme_check/json_check.rb +2 -2
- data/lib/theme_check/language_server/bridge.rb +128 -0
- data/lib/theme_check/language_server/channel.rb +69 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +3 -1
- data/lib/theme_check/language_server/diagnostics_engine.rb +125 -0
- data/lib/theme_check/language_server/diagnostics_tracker.rb +8 -8
- data/lib/theme_check/language_server/handler.rb +20 -117
- data/lib/theme_check/language_server/io_messenger.rb +97 -0
- data/lib/theme_check/language_server/messenger.rb +27 -0
- data/lib/theme_check/language_server/server.rb +95 -104
- data/lib/theme_check/language_server.rb +6 -1
- data/lib/theme_check/{template.rb → liquid_file.rb} +2 -2
- data/lib/theme_check/liquid_node.rb +291 -0
- data/lib/theme_check/{visitor.rb → liquid_visitor.rb} +4 -4
- data/lib/theme_check/locale_diff.rb +14 -7
- data/lib/theme_check/node.rb +12 -225
- data/lib/theme_check/offense.rb +15 -15
- data/lib/theme_check/position.rb +1 -1
- 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/theme.rb +1 -1
- data/lib/theme_check/{template_rewriter.rb → theme_file_rewriter.rb} +1 -1
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check.rb +11 -10
- data/theme-check.gemspec +1 -1
- metadata +14 -7
@@ -4,11 +4,11 @@
|
|
4
4
|
# We'll use the node position to figure out if the test is disabled or not.
|
5
5
|
module ThemeCheck
|
6
6
|
class DisabledCheck
|
7
|
-
attr_reader :name, :
|
7
|
+
attr_reader :name, :theme_file, :ranges
|
8
8
|
attr_accessor :first_line
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@
|
10
|
+
def initialize(theme_file, name)
|
11
|
+
@theme_file = theme_file
|
12
12
|
@name = name
|
13
13
|
@ranges = []
|
14
14
|
@first_line = false
|
@@ -11,8 +11,8 @@ module ThemeCheck
|
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
@disabled_checks = Hash.new do |hash, key|
|
14
|
-
|
15
|
-
hash[key] = DisabledCheck.new(
|
14
|
+
theme_file, check_name = key
|
15
|
+
hash[key] = DisabledCheck.new(theme_file, check_name)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -20,26 +20,26 @@ module ThemeCheck
|
|
20
20
|
text = comment_text(node)
|
21
21
|
if start_disabling?(text)
|
22
22
|
checks_from_text(text).each do |check_name|
|
23
|
-
disabled = @disabled_checks[[node.
|
23
|
+
disabled = @disabled_checks[[node.theme_file, check_name]]
|
24
24
|
disabled.start_index = node.start_index
|
25
25
|
disabled.first_line = true if node.line_number == 1
|
26
26
|
end
|
27
27
|
elsif stop_disabling?(text)
|
28
28
|
checks_from_text(text).each do |check_name|
|
29
|
-
disabled = @disabled_checks[[node.
|
29
|
+
disabled = @disabled_checks[[node.theme_file, check_name]]
|
30
30
|
next unless disabled
|
31
31
|
disabled.end_index = node.end_index
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def disabled?(check,
|
36
|
+
def disabled?(check, theme_file, check_name, index)
|
37
37
|
return true if check.ignored_patterns&.any? do |pattern|
|
38
|
-
|
38
|
+
theme_file.relative_path.fnmatch?(pattern)
|
39
39
|
end
|
40
40
|
|
41
|
-
@disabled_checks[[
|
42
|
-
@disabled_checks[[
|
41
|
+
@disabled_checks[[theme_file, :all]]&.disabled?(index) ||
|
42
|
+
@disabled_checks[[theme_file, check_name]]&.disabled?(index)
|
43
43
|
end
|
44
44
|
|
45
45
|
def checks_missing_end_index
|
@@ -51,7 +51,7 @@ module ThemeCheck
|
|
51
51
|
def remove_disabled_offenses(checks)
|
52
52
|
checks.disableable.each do |check|
|
53
53
|
check.offenses.reject! do |offense|
|
54
|
-
disabled?(check, offense.
|
54
|
+
disabled?(check, offense.theme_file, offense.code_name, offense.start_index)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -2,18 +2,50 @@
|
|
2
2
|
require "forwardable"
|
3
3
|
|
4
4
|
module ThemeCheck
|
5
|
-
class HtmlNode
|
5
|
+
class HtmlNode < Node
|
6
6
|
extend Forwardable
|
7
7
|
include RegexHelpers
|
8
|
-
attr_reader :
|
8
|
+
attr_reader :theme_file, :parent
|
9
9
|
|
10
|
-
def initialize(value,
|
10
|
+
def initialize(value, theme_file, placeholder_values = [], parent = nil)
|
11
11
|
@value = value
|
12
|
-
@
|
12
|
+
@theme_file = theme_file
|
13
13
|
@placeholder_values = placeholder_values
|
14
14
|
@parent = parent
|
15
15
|
end
|
16
16
|
|
17
|
+
# @value is not forwarded because we _need_ to replace the
|
18
|
+
# placeholders for the HtmlNode to make sense.
|
19
|
+
def value
|
20
|
+
if literal?
|
21
|
+
content
|
22
|
+
else
|
23
|
+
markup
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def children
|
28
|
+
@children ||= @value
|
29
|
+
.children
|
30
|
+
.map { |child| HtmlNode.new(child, theme_file, @placeholder_values, self) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def markup
|
34
|
+
@markup ||= replace_placeholders(@value.to_html)
|
35
|
+
end
|
36
|
+
|
37
|
+
def line_number
|
38
|
+
@value.line
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_index
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
|
45
|
+
def end_index
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
17
49
|
def literal?
|
18
50
|
@value.name == "text"
|
19
51
|
end
|
@@ -22,12 +54,6 @@ module ThemeCheck
|
|
22
54
|
@value.element?
|
23
55
|
end
|
24
56
|
|
25
|
-
def children
|
26
|
-
@children ||= @value
|
27
|
-
.children
|
28
|
-
.map { |child| HtmlNode.new(child, template, @placeholder_values, self) }
|
29
|
-
end
|
30
|
-
|
31
57
|
def attributes
|
32
58
|
@attributes ||= @value.attributes
|
33
59
|
.map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
|
@@ -38,16 +64,6 @@ module ThemeCheck
|
|
38
64
|
@content ||= replace_placeholders(@value.content)
|
39
65
|
end
|
40
66
|
|
41
|
-
# @value is not forwarded because we _need_ to replace the
|
42
|
-
# placeholders for the HtmlNode to make sense.
|
43
|
-
def value
|
44
|
-
if literal?
|
45
|
-
content
|
46
|
-
else
|
47
|
-
markup
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
67
|
def name
|
52
68
|
if @value.name == "#document-fragment"
|
53
69
|
"document"
|
@@ -56,14 +72,6 @@ module ThemeCheck
|
|
56
72
|
end
|
57
73
|
end
|
58
74
|
|
59
|
-
def markup
|
60
|
-
@markup ||= replace_placeholders(@value.to_html)
|
61
|
-
end
|
62
|
-
|
63
|
-
def line_number
|
64
|
-
@value.line
|
65
|
-
end
|
66
|
-
|
67
75
|
private
|
68
76
|
|
69
77
|
def replace_placeholders(string)
|
@@ -11,18 +11,18 @@ module ThemeCheck
|
|
11
11
|
@checks = checks
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
doc, placeholder_values = parse(
|
16
|
-
visit(HtmlNode.new(doc,
|
14
|
+
def visit_liquid_file(liquid_file)
|
15
|
+
doc, placeholder_values = parse(liquid_file)
|
16
|
+
visit(HtmlNode.new(doc, liquid_file, placeholder_values))
|
17
17
|
rescue ArgumentError => e
|
18
|
-
call_checks(:on_parse_error, e,
|
18
|
+
call_checks(:on_parse_error, e, liquid_file)
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
def parse(
|
23
|
+
def parse(liquid_file)
|
24
24
|
placeholder_values = []
|
25
|
-
parseable_source = +
|
25
|
+
parseable_source = +liquid_file.source.clone
|
26
26
|
|
27
27
|
# Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
|
28
28
|
# parser from freaking out. We transparently replace those placeholders in
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# An in-memory storage is not written to disk. The reasons why you'd
|
4
4
|
# want to do that are your own. The idea is to not write to disk
|
5
|
-
# something that doesn't need to be there. If you have your
|
5
|
+
# something that doesn't need to be there. If you have your theme
|
6
6
|
# as a big hash already, leave it like that and save yourself some IO.
|
7
7
|
module ThemeCheck
|
8
8
|
class InMemoryStorage < Storage
|
@@ -4,8 +4,8 @@ module ThemeCheck
|
|
4
4
|
class JsonCheck < Check
|
5
5
|
extend ChecksTracking
|
6
6
|
|
7
|
-
def add_offense(message, markup: nil, line_number: nil,
|
8
|
-
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number,
|
7
|
+
def add_offense(message, markup: nil, line_number: nil, theme_file: nil, &block)
|
8
|
+
offenses << Offense.new(check: self, message: message, markup: markup, line_number: line_number, theme_file: theme_file, correction: block)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class exists as a bridge (or boundary) between our handlers and the outside world.
|
4
|
+
#
|
5
|
+
# It is concerned with all the Language Server Protocol constructs. i.e.
|
6
|
+
#
|
7
|
+
# - sending Hash messages as JSON
|
8
|
+
# - reading JSON messages as Hashes
|
9
|
+
# - preparing, sending and resolving requests
|
10
|
+
# - preparing and sending responses
|
11
|
+
# - preparing and sending notifications
|
12
|
+
# - preparing and sending progress notifications
|
13
|
+
#
|
14
|
+
# But it _not_ concerned by _how_ those messages are sent to the
|
15
|
+
# outside world. That's the job of the messenger.
|
16
|
+
#
|
17
|
+
# This enables us to have all the language server protocol logic
|
18
|
+
# in here living independently of how we communicate with the
|
19
|
+
# client (STDIO or websocket)
|
20
|
+
module ThemeCheck
|
21
|
+
module LanguageServer
|
22
|
+
class Bridge
|
23
|
+
attr_writer :supports_work_done_progress
|
24
|
+
|
25
|
+
def initialize(messenger)
|
26
|
+
# The messenger is responsible for IO.
|
27
|
+
# Could be STDIO or WebSockets or Mock.
|
28
|
+
@messenger = messenger
|
29
|
+
|
30
|
+
# Whether the client supports work done progress notifications
|
31
|
+
@supports_work_done_progress = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def log(message)
|
35
|
+
@messenger.log(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_message
|
39
|
+
message_body = @messenger.read_message
|
40
|
+
message_json = JSON.parse(message_body)
|
41
|
+
@messenger.log(JSON.pretty_generate(message_json)) if $DEBUG
|
42
|
+
message_json
|
43
|
+
end
|
44
|
+
|
45
|
+
def send_message(message_hash)
|
46
|
+
message_hash[:jsonrpc] = '2.0'
|
47
|
+
message_body = JSON.dump(message_hash)
|
48
|
+
@messenger.log(JSON.pretty_generate(message_hash)) if $DEBUG
|
49
|
+
@messenger.send_message(message_body)
|
50
|
+
end
|
51
|
+
|
52
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#requestMessage
|
53
|
+
def send_request(method, params = nil)
|
54
|
+
channel = Channel.create
|
55
|
+
message = { id: channel.id }
|
56
|
+
message[:method] = method
|
57
|
+
message[:params] = params if params
|
58
|
+
send_message(message)
|
59
|
+
channel.pop
|
60
|
+
ensure
|
61
|
+
channel.close
|
62
|
+
end
|
63
|
+
|
64
|
+
def receive_response(id, result)
|
65
|
+
Channel.by_id(id) << result
|
66
|
+
end
|
67
|
+
|
68
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
|
69
|
+
def send_response(id, result = nil, error = nil)
|
70
|
+
message = { id: id }
|
71
|
+
message[:result] = result if result
|
72
|
+
message[:error] = error if error
|
73
|
+
send_message(message)
|
74
|
+
end
|
75
|
+
|
76
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
77
|
+
def send_notification(method, params)
|
78
|
+
message = { method: method }
|
79
|
+
message[:params] = params
|
80
|
+
send_message(message)
|
81
|
+
end
|
82
|
+
|
83
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
|
84
|
+
def send_progress(token, value)
|
85
|
+
send_notification("$/progress", token: token, value: value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def supports_work_done_progress?
|
89
|
+
@supports_work_done_progress
|
90
|
+
end
|
91
|
+
|
92
|
+
def send_create_work_done_progress_request(token)
|
93
|
+
return unless supports_work_done_progress?
|
94
|
+
send_request("window/workDoneProgress/create", {
|
95
|
+
token: token,
|
96
|
+
})
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_work_done_progress_begin(token, title)
|
100
|
+
return unless supports_work_done_progress?
|
101
|
+
send_progress(token, {
|
102
|
+
kind: 'begin',
|
103
|
+
title: title,
|
104
|
+
cancellable: false,
|
105
|
+
percentage: 0,
|
106
|
+
})
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_work_done_progress_report(token, message, percentage)
|
110
|
+
return unless supports_work_done_progress?
|
111
|
+
send_progress(token, {
|
112
|
+
kind: 'report',
|
113
|
+
message: message,
|
114
|
+
cancellable: false,
|
115
|
+
percentage: percentage,
|
116
|
+
})
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_work_done_progress_end(token, message)
|
120
|
+
return unless supports_work_done_progress?
|
121
|
+
send_progress(token, {
|
122
|
+
kind: 'end',
|
123
|
+
message: message,
|
124
|
+
})
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
# How you'd use this class:
|
6
|
+
#
|
7
|
+
# In thread #1:
|
8
|
+
# def foo
|
9
|
+
# chan = Channel.create
|
10
|
+
# send_request(chan.id, ...)
|
11
|
+
# result = chan.pop
|
12
|
+
# do_stuff_with_result(result)
|
13
|
+
# ensure
|
14
|
+
# chan.close
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# In thread #2:
|
18
|
+
# Channel.by_id(id) << result
|
19
|
+
class Channel
|
20
|
+
MUTEX = Mutex.new
|
21
|
+
CHANNELS = {}
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def create
|
25
|
+
id = new_id
|
26
|
+
CHANNELS[id] = new(id)
|
27
|
+
CHANNELS[id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def by_id(id)
|
31
|
+
CHANNELS[id]
|
32
|
+
end
|
33
|
+
|
34
|
+
def close(id)
|
35
|
+
CHANNELS.delete(id)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def new_id
|
41
|
+
MUTEX.synchronize do
|
42
|
+
@id ||= 0
|
43
|
+
@id += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :id
|
49
|
+
|
50
|
+
def initialize(id)
|
51
|
+
@id = id
|
52
|
+
@response = SizedQueue.new(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
def pop
|
56
|
+
@response.pop
|
57
|
+
end
|
58
|
+
|
59
|
+
def <<(value)
|
60
|
+
@response << value
|
61
|
+
end
|
62
|
+
|
63
|
+
def close
|
64
|
+
@response.close
|
65
|
+
Channel.close(id)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
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,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class DiagnosticsEngine
|
6
|
+
include URIHelper
|
7
|
+
|
8
|
+
def initialize(bridge)
|
9
|
+
@diagnostics_lock = Mutex.new
|
10
|
+
@diagnostics_tracker = DiagnosticsTracker.new
|
11
|
+
@bridge = bridge
|
12
|
+
@token = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def first_run?
|
16
|
+
@diagnostics_tracker.first_run?
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyze_and_send_offenses(absolute_path, config)
|
20
|
+
return unless @diagnostics_lock.try_lock
|
21
|
+
@token += 1
|
22
|
+
@bridge.send_create_work_done_progress_request(@token)
|
23
|
+
storage = ThemeCheck::FileSystemStorage.new(
|
24
|
+
config.root,
|
25
|
+
ignored_patterns: config.ignored_patterns
|
26
|
+
)
|
27
|
+
theme = ThemeCheck::Theme.new(storage)
|
28
|
+
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
29
|
+
|
30
|
+
if @diagnostics_tracker.first_run?
|
31
|
+
@bridge.send_work_done_progress_begin(@token, "Full theme check")
|
32
|
+
@bridge.log("Checking #{config.root}")
|
33
|
+
offenses = nil
|
34
|
+
time = Benchmark.measure do
|
35
|
+
offenses = analyzer.analyze_theme do |path, i, total|
|
36
|
+
@bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
|
40
|
+
@bridge.send_work_done_progress_end(@token, end_message)
|
41
|
+
send_diagnostics(offenses)
|
42
|
+
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]) 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, [absolute_path])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
@diagnostics_lock.unlock
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def send_diagnostics(offenses, analyzed_files = nil)
|
67
|
+
@diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
|
68
|
+
send_diagnostic(path, diagnostic_offenses)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_diagnostic(path, offenses)
|
73
|
+
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
74
|
+
@bridge.send_notification('textDocument/publishDiagnostics', {
|
75
|
+
uri: file_uri(path),
|
76
|
+
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
77
|
+
})
|
78
|
+
end
|
79
|
+
|
80
|
+
def offense_to_diagnostic(offense)
|
81
|
+
diagnostic = {
|
82
|
+
code: offense.code_name,
|
83
|
+
message: offense.message,
|
84
|
+
range: range(offense),
|
85
|
+
severity: severity(offense),
|
86
|
+
source: "theme-check",
|
87
|
+
}
|
88
|
+
diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
|
89
|
+
diagnostic
|
90
|
+
end
|
91
|
+
|
92
|
+
def code_description(offense)
|
93
|
+
{
|
94
|
+
href: offense.doc,
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def severity(offense)
|
99
|
+
case offense.severity
|
100
|
+
when :error
|
101
|
+
1
|
102
|
+
when :suggestion
|
103
|
+
2
|
104
|
+
when :style
|
105
|
+
3
|
106
|
+
else
|
107
|
+
4
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def range(offense)
|
112
|
+
{
|
113
|
+
start: {
|
114
|
+
line: offense.start_line,
|
115
|
+
character: offense.start_column,
|
116
|
+
},
|
117
|
+
end: {
|
118
|
+
line: offense.end_line,
|
119
|
+
character: offense.end_column,
|
120
|
+
},
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -19,22 +19,22 @@ module ThemeCheck
|
|
19
19
|
new_single_file_offenses = {}
|
20
20
|
analyzed_files = analyzed_files.map { |path| Pathname.new(path) } if analyzed_files
|
21
21
|
|
22
|
-
offenses.group_by(&:
|
23
|
-
next unless
|
22
|
+
offenses.group_by(&:theme_file).each do |theme_file, template_offenses|
|
23
|
+
next unless theme_file
|
24
24
|
reported_offenses = template_offenses
|
25
|
-
previous_offenses = @single_files_offenses[
|
26
|
-
if analyzed_files.nil? || analyzed_files.include?(
|
25
|
+
previous_offenses = @single_files_offenses[theme_file.path]
|
26
|
+
if analyzed_files.nil? || analyzed_files.include?(theme_file.path)
|
27
27
|
# We re-analyzed the file, so we know the template_offenses are update to date.
|
28
28
|
reported_single_file_offenses = reported_offenses.select(&:single_file?)
|
29
29
|
if reported_single_file_offenses.any?
|
30
|
-
new_single_file_offenses[
|
30
|
+
new_single_file_offenses[theme_file.path] = reported_single_file_offenses
|
31
31
|
end
|
32
32
|
elsif previous_offenses
|
33
33
|
# Merge in the previous ones, if some
|
34
34
|
reported_offenses |= previous_offenses
|
35
35
|
end
|
36
|
-
yield
|
37
|
-
reported_files <<
|
36
|
+
yield theme_file.path, reported_offenses
|
37
|
+
reported_files << theme_file.path
|
38
38
|
end
|
39
39
|
|
40
40
|
@single_files_offenses.each do |path, _|
|
@@ -51,7 +51,7 @@ module ThemeCheck
|
|
51
51
|
reported_files << path
|
52
52
|
end
|
53
53
|
|
54
|
-
# Publish diagnostics with empty array if all issues on a previously reported
|
54
|
+
# Publish diagnostics with empty array if all issues on a previously reported theme_file
|
55
55
|
# have been fixed.
|
56
56
|
(@previously_reported_files - reported_files).each do |path|
|
57
57
|
yield path, []
|