theme-check 0.8.0 → 0.9.1
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/workflows/theme-check.yml +3 -0
- data/CHANGELOG.md +44 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/config/default.yml +42 -1
- data/data/shopify_liquid/tags.yml +3 -0
- data/data/shopify_translation_keys.yml +1 -0
- data/docs/checks/asset_url_filters.md +56 -0
- data/docs/checks/content_for_header_modification.md +42 -0
- data/docs/checks/nested_snippet.md +1 -1
- data/docs/checks/parser_blocking_script_tag.md +53 -0
- data/docs/checks/space_inside_braces.md +28 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +13 -1
- data/lib/theme_check/analyzer.rb +79 -13
- data/lib/theme_check/bug.rb +20 -0
- data/lib/theme_check/check.rb +36 -7
- data/lib/theme_check/checks.rb +47 -8
- data/lib/theme_check/checks/asset_url_filters.rb +46 -0
- data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
- data/lib/theme_check/checks/img_width_and_height.rb +18 -49
- data/lib/theme_check/checks/missing_enable_comment.rb +4 -4
- data/lib/theme_check/checks/missing_template.rb +1 -0
- data/lib/theme_check/checks/nested_snippet.rb +1 -1
- data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
- data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
- data/lib/theme_check/checks/remote_asset.rb +21 -79
- data/lib/theme_check/checks/space_inside_braces.rb +8 -2
- data/lib/theme_check/checks/template_length.rb +3 -0
- data/lib/theme_check/checks/valid_html_translation.rb +1 -0
- data/lib/theme_check/config.rb +2 -0
- data/lib/theme_check/disabled_check.rb +41 -0
- data/lib/theme_check/disabled_checks.rb +33 -29
- data/lib/theme_check/exceptions.rb +32 -0
- data/lib/theme_check/html_check.rb +7 -0
- data/lib/theme_check/html_node.rb +56 -0
- data/lib/theme_check/html_visitor.rb +38 -0
- data/lib/theme_check/json_file.rb +13 -1
- data/lib/theme_check/language_server.rb +2 -1
- data/lib/theme_check/language_server/completion_engine.rb +1 -1
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -8
- data/lib/theme_check/language_server/constants.rb +5 -1
- data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
- data/lib/theme_check/language_server/document_link_engine.rb +2 -2
- data/lib/theme_check/language_server/handler.rb +63 -50
- data/lib/theme_check/language_server/server.rb +1 -1
- data/lib/theme_check/language_server/variable_lookup_finder.rb +295 -0
- data/lib/theme_check/liquid_check.rb +1 -4
- data/lib/theme_check/node.rb +12 -0
- data/lib/theme_check/offense.rb +30 -46
- data/lib/theme_check/position.rb +77 -0
- data/lib/theme_check/position_helper.rb +37 -0
- data/lib/theme_check/remote_asset_file.rb +3 -0
- data/lib/theme_check/shopify_liquid/tag.rb +13 -0
- data/lib/theme_check/template.rb +8 -0
- data/lib/theme_check/theme.rb +7 -2
- data/lib/theme_check/version.rb +1 -1
- data/lib/theme_check/visitor.rb +4 -14
- metadata +19 -4
- data/lib/theme_check/language_server/position_helper.rb +0 -27
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
class HtmlNode
|
6
|
+
extend Forwardable
|
7
|
+
attr_reader :template
|
8
|
+
|
9
|
+
def_delegators :@value, :content, :attributes
|
10
|
+
|
11
|
+
def initialize(value, template)
|
12
|
+
@value = value
|
13
|
+
@template = template
|
14
|
+
end
|
15
|
+
|
16
|
+
def literal?
|
17
|
+
@value.name == "text"
|
18
|
+
end
|
19
|
+
|
20
|
+
def element?
|
21
|
+
@value.element?
|
22
|
+
end
|
23
|
+
|
24
|
+
def children
|
25
|
+
@value.children.map { |child| HtmlNode.new(child, template) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def parent
|
29
|
+
HtmlNode.new(@value.parent, template)
|
30
|
+
end
|
31
|
+
|
32
|
+
def name
|
33
|
+
if @value.name == "#document-fragment"
|
34
|
+
"document"
|
35
|
+
else
|
36
|
+
@value.name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def value
|
41
|
+
if literal?
|
42
|
+
@value.content
|
43
|
+
else
|
44
|
+
@value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def markup
|
49
|
+
@value.to_html
|
50
|
+
end
|
51
|
+
|
52
|
+
def line_number
|
53
|
+
@value.line
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "nokogumbo"
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module ThemeCheck
|
6
|
+
class HtmlVisitor
|
7
|
+
attr_reader :checks
|
8
|
+
|
9
|
+
def initialize(checks)
|
10
|
+
@checks = checks
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_template(template)
|
14
|
+
doc = parse(template)
|
15
|
+
visit(HtmlNode.new(doc, template))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse(template)
|
21
|
+
Nokogiri::HTML5.fragment(template.source)
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit(node)
|
25
|
+
call_checks(:on_element, node) if node.element?
|
26
|
+
call_checks(:"on_#{node.name}", node)
|
27
|
+
node.children.each { |child| visit(child) }
|
28
|
+
unless node.literal?
|
29
|
+
call_checks(:"after_#{node.name}", node)
|
30
|
+
call_checks(:after_element, node) if node.element?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_checks(method, *args)
|
35
|
+
checks.call(method, *args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -20,6 +20,10 @@ module ThemeCheck
|
|
20
20
|
@relative_pathname ||= Pathname.new(@relative_path)
|
21
21
|
end
|
22
22
|
|
23
|
+
def source
|
24
|
+
@source ||= @storage.read(@relative_path)
|
25
|
+
end
|
26
|
+
|
23
27
|
def content
|
24
28
|
load!
|
25
29
|
@content
|
@@ -34,12 +38,20 @@ module ThemeCheck
|
|
34
38
|
relative_path.sub_ext('').to_s
|
35
39
|
end
|
36
40
|
|
41
|
+
def json?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def liquid?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
37
49
|
private
|
38
50
|
|
39
51
|
def load!
|
40
52
|
return if @loaded
|
41
53
|
|
42
|
-
@content = JSON.parse(
|
54
|
+
@content = JSON.parse(source)
|
43
55
|
rescue JSON::ParserError => e
|
44
56
|
@parser_error = e
|
45
57
|
ensure
|
@@ -4,11 +4,12 @@ require_relative "language_server/constants"
|
|
4
4
|
require_relative "language_server/handler"
|
5
5
|
require_relative "language_server/server"
|
6
6
|
require_relative "language_server/tokens"
|
7
|
-
require_relative "language_server/
|
7
|
+
require_relative "language_server/variable_lookup_finder"
|
8
8
|
require_relative "language_server/completion_helper"
|
9
9
|
require_relative "language_server/completion_provider"
|
10
10
|
require_relative "language_server/completion_engine"
|
11
11
|
require_relative "language_server/document_link_engine"
|
12
|
+
require_relative "language_server/diagnostics_tracker"
|
12
13
|
|
13
14
|
Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
|
14
15
|
require file
|
@@ -12,7 +12,7 @@ module ThemeCheck
|
|
12
12
|
|
13
13
|
def completions(relative_path, line, col)
|
14
14
|
buffer = @storage.read(relative_path)
|
15
|
-
cursor =
|
15
|
+
cursor = from_row_column_to_index(buffer, line, col)
|
16
16
|
token = find_token(buffer, cursor)
|
17
17
|
return [] if token.nil?
|
18
18
|
|
@@ -4,18 +4,20 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
class ObjectCompletionProvider < CompletionProvider
|
6
6
|
def completions(content, cursor)
|
7
|
-
return [] unless
|
8
|
-
|
7
|
+
return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
|
8
|
+
return [] unless variable_lookup.lookups.empty?
|
9
|
+
return [] if content[cursor - 1] == "."
|
9
10
|
ShopifyLiquid::Object.labels
|
10
|
-
.select { |w| w.start_with?(partial) }
|
11
|
+
.select { |w| w.start_with?(partial(variable_lookup)) }
|
11
12
|
.map { |object| object_to_completion(object) }
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def variable_lookup_at_cursor(content, cursor)
|
16
|
+
VariableLookupFinder.lookup(content, cursor)
|
17
|
+
end
|
18
|
+
|
19
|
+
def partial(variable_lookup)
|
20
|
+
variable_lookup.name || ''
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
@@ -4,7 +4,11 @@ module ThemeCheck
|
|
4
4
|
module LanguageServer
|
5
5
|
PARTIAL_RENDER = %r{
|
6
6
|
\{\%-?\s*render\s+'(?<partial>[^']*)'|
|
7
|
-
\{\%-?\s*render\s+"(?<partial>[^"]*)"
|
7
|
+
\{\%-?\s*render\s+"(?<partial>[^"]*)"|
|
8
|
+
|
9
|
+
# in liquid tags the whole line is white space until render
|
10
|
+
^\s*render\s+'(?<partial>[^']*)'|
|
11
|
+
^\s*render\s+"(?<partial>[^"]*)"
|
8
12
|
}mix
|
9
13
|
end
|
10
14
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThemeCheck
|
4
|
+
module LanguageServer
|
5
|
+
class DiagnosticsTracker
|
6
|
+
def initialize
|
7
|
+
@previously_reported_files = Set.new
|
8
|
+
@single_files_offenses = {}
|
9
|
+
@first_run = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def first_run?
|
13
|
+
@first_run
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_diagnostics(offenses, analyzed_files: nil)
|
17
|
+
reported_files = Set.new
|
18
|
+
new_single_file_offenses = {}
|
19
|
+
|
20
|
+
offenses.group_by(&:template).each do |template, template_offenses|
|
21
|
+
next unless template
|
22
|
+
reported_offenses = template_offenses
|
23
|
+
previous_offenses = @single_files_offenses[template.path]
|
24
|
+
if analyzed_files.nil? || analyzed_files.include?(template.path)
|
25
|
+
# We re-analyzed the file, so we know the template_offenses are update to date.
|
26
|
+
reported_single_file_offenses = reported_offenses.select(&:single_file?)
|
27
|
+
if reported_single_file_offenses.any?
|
28
|
+
new_single_file_offenses[template.path] = reported_single_file_offenses
|
29
|
+
end
|
30
|
+
elsif previous_offenses
|
31
|
+
# Merge in the previous ones, if some
|
32
|
+
reported_offenses |= previous_offenses
|
33
|
+
end
|
34
|
+
yield template.path, reported_offenses
|
35
|
+
reported_files << template.path
|
36
|
+
end
|
37
|
+
|
38
|
+
@single_files_offenses.each do |path, _|
|
39
|
+
# Already reported above, skip
|
40
|
+
next if reported_files.include?(path)
|
41
|
+
|
42
|
+
if analyzed_files.nil? || analyzed_files.include?(path)
|
43
|
+
# We re-analyzed this file, if it was not reported, all offenses in it got fixed
|
44
|
+
yield path, []
|
45
|
+
new_single_file_offenses[path] = nil
|
46
|
+
end
|
47
|
+
# NOTE: No need to re-report previous offenses as LSP should keep them around until
|
48
|
+
# we clear them.
|
49
|
+
reported_files << path
|
50
|
+
end
|
51
|
+
|
52
|
+
# Publish diagnostics with empty array if all issues on a previously reported template
|
53
|
+
# have been fixed.
|
54
|
+
(@previously_reported_files - reported_files).each do |path|
|
55
|
+
yield path, []
|
56
|
+
end
|
57
|
+
|
58
|
+
@previously_reported_files = reported_files
|
59
|
+
@single_files_offenses.merge!(new_single_file_offenses)
|
60
|
+
@first_run = false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -14,12 +14,12 @@ module ThemeCheck
|
|
14
14
|
buffer = @storage.read(relative_path)
|
15
15
|
return [] unless buffer
|
16
16
|
matches(buffer, PARTIAL_RENDER).map do |match|
|
17
|
-
start_line, start_character =
|
17
|
+
start_line, start_character = from_index_to_row_column(
|
18
18
|
buffer,
|
19
19
|
match.begin(:partial),
|
20
20
|
)
|
21
21
|
|
22
|
-
end_line, end_character =
|
22
|
+
end_line, end_character = from_index_to_row_column(
|
23
23
|
buffer,
|
24
24
|
match.end(:partial)
|
25
25
|
)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "benchmark"
|
2
3
|
|
3
4
|
module ThemeCheck
|
4
5
|
module LanguageServer
|
@@ -19,21 +20,21 @@ module ThemeCheck
|
|
19
20
|
|
20
21
|
def initialize(server)
|
21
22
|
@server = server
|
22
|
-
@
|
23
|
+
@diagnostics_tracker = DiagnosticsTracker.new
|
23
24
|
end
|
24
25
|
|
25
26
|
def on_initialize(id, params)
|
26
|
-
@root_path = params["rootPath"]
|
27
|
+
@root_path = path_from_uri(params["rootUri"]) || params["rootPath"]
|
28
|
+
|
29
|
+
# Tell the client we don't support anything if there's no rootPath
|
30
|
+
return send_response(id, { capabilities: {} }) if @root_path.nil?
|
27
31
|
@storage = in_memory_storage(@root_path)
|
28
32
|
@completion_engine = CompletionEngine.new(@storage)
|
29
33
|
@document_link_engine = DocumentLinkEngine.new(@storage)
|
30
34
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
|
31
|
-
send_response(
|
32
|
-
|
33
|
-
|
34
|
-
capabilities: CAPABILITIES,
|
35
|
-
}
|
36
|
-
)
|
35
|
+
send_response(id, {
|
36
|
+
capabilities: CAPABILITIES,
|
37
|
+
})
|
37
38
|
end
|
38
39
|
|
39
40
|
def on_exit(_id, _params)
|
@@ -52,6 +53,7 @@ module ThemeCheck
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def on_text_document_did_open(_id, params)
|
56
|
+
return unless @diagnostics_tracker.first_run?
|
55
57
|
relative_path = relative_path_from_text_document_uri(params)
|
56
58
|
@storage.write(relative_path, text_document_text(params))
|
57
59
|
analyze_and_send_offenses(text_document_uri(params))
|
@@ -63,20 +65,14 @@ module ThemeCheck
|
|
63
65
|
|
64
66
|
def on_text_document_document_link(id, params)
|
65
67
|
relative_path = relative_path_from_text_document_uri(params)
|
66
|
-
send_response(
|
67
|
-
id: id,
|
68
|
-
result: document_links(relative_path)
|
69
|
-
)
|
68
|
+
send_response(id, document_links(relative_path))
|
70
69
|
end
|
71
70
|
|
72
71
|
def on_text_document_completion(id, params)
|
73
72
|
relative_path = relative_path_from_text_document_uri(params)
|
74
73
|
line = params.dig('position', 'line')
|
75
74
|
col = params.dig('position', 'character')
|
76
|
-
send_response(
|
77
|
-
id: id,
|
78
|
-
result: completions(relative_path, line, col)
|
79
|
-
)
|
75
|
+
send_response(id, completions(relative_path, line, col))
|
80
76
|
end
|
81
77
|
|
82
78
|
private
|
@@ -99,7 +95,11 @@ module ThemeCheck
|
|
99
95
|
end
|
100
96
|
|
101
97
|
def text_document_uri(params)
|
102
|
-
params.dig('textDocument', 'uri')
|
98
|
+
path_from_uri(params.dig('textDocument', 'uri'))
|
99
|
+
end
|
100
|
+
|
101
|
+
def path_from_uri(uri)
|
102
|
+
uri&.sub('file://', '')
|
103
103
|
end
|
104
104
|
|
105
105
|
def relative_path_from_text_document_uri(params)
|
@@ -126,17 +126,32 @@ module ThemeCheck
|
|
126
126
|
ignored_patterns: config.ignored_patterns
|
127
127
|
)
|
128
128
|
theme = ThemeCheck::Theme.new(storage)
|
129
|
-
|
130
|
-
offenses = analyze(theme, config)
|
131
|
-
log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
|
132
|
-
send_diagnostics(offenses)
|
133
|
-
end
|
134
|
-
|
135
|
-
def analyze(theme, config)
|
136
129
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
137
|
-
|
138
|
-
|
139
|
-
|
130
|
+
|
131
|
+
if @diagnostics_tracker.first_run?
|
132
|
+
# Analyze the full theme on first run
|
133
|
+
log("Checking #{config.root}")
|
134
|
+
offenses = nil
|
135
|
+
time = Benchmark.measure do
|
136
|
+
offenses = analyzer.analyze_theme
|
137
|
+
end
|
138
|
+
log("Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s")
|
139
|
+
send_diagnostics(offenses)
|
140
|
+
else
|
141
|
+
# Analyze selected files
|
142
|
+
relative_path = Pathname.new(@storage.relative_path(absolute_path))
|
143
|
+
file = theme[relative_path]
|
144
|
+
# Skip if not a theme file
|
145
|
+
if file
|
146
|
+
log("Checking #{relative_path}")
|
147
|
+
offenses = nil
|
148
|
+
time = Benchmark.measure do
|
149
|
+
offenses = analyzer.analyze_files([file])
|
150
|
+
end
|
151
|
+
log("Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s")
|
152
|
+
send_diagnostics(offenses, [absolute_path])
|
153
|
+
end
|
154
|
+
end
|
140
155
|
end
|
141
156
|
|
142
157
|
def completions(relative_path, line, col)
|
@@ -147,33 +162,18 @@ module ThemeCheck
|
|
147
162
|
@document_link_engine.document_links(relative_path)
|
148
163
|
end
|
149
164
|
|
150
|
-
def send_diagnostics(offenses)
|
151
|
-
|
152
|
-
|
153
|
-
offenses.group_by(&:template).each do |template, template_offenses|
|
154
|
-
next unless template
|
155
|
-
send_diagnostic(template.path, template_offenses)
|
156
|
-
reported_files << template.path
|
157
|
-
end
|
158
|
-
|
159
|
-
# Publish diagnostics with empty array if all issues on a previously reported template
|
160
|
-
# have been solved.
|
161
|
-
(@previously_reported_files - reported_files).each do |path|
|
162
|
-
send_diagnostic(path, [])
|
165
|
+
def send_diagnostics(offenses, analyzed_files = nil)
|
166
|
+
@diagnostics_tracker.build_diagnostics(offenses, analyzed_files: analyzed_files) do |path, diagnostic_offenses|
|
167
|
+
send_diagnostic(path, diagnostic_offenses)
|
163
168
|
end
|
164
|
-
|
165
|
-
@previously_reported_files = reported_files
|
166
169
|
end
|
167
170
|
|
168
171
|
def send_diagnostic(path, offenses)
|
169
172
|
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
175
|
-
},
|
176
|
-
)
|
173
|
+
send_notification('textDocument/publishDiagnostics', {
|
174
|
+
uri: "file://#{path}",
|
175
|
+
diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
|
176
|
+
})
|
177
177
|
end
|
178
178
|
|
179
179
|
def offense_to_diagnostic(offense)
|
@@ -220,11 +220,24 @@ module ThemeCheck
|
|
220
220
|
}
|
221
221
|
end
|
222
222
|
|
223
|
-
def
|
223
|
+
def send_message(message)
|
224
224
|
message[:jsonrpc] = '2.0'
|
225
225
|
@server.send_response(message)
|
226
226
|
end
|
227
227
|
|
228
|
+
def send_response(id, result = nil, error = nil)
|
229
|
+
message = { id: id }
|
230
|
+
message[:result] = result if result
|
231
|
+
message[:error] = error if error
|
232
|
+
send_message(message)
|
233
|
+
end
|
234
|
+
|
235
|
+
def send_notification(method, params)
|
236
|
+
message = { method: method }
|
237
|
+
message[:params] = params
|
238
|
+
send_message(message)
|
239
|
+
end
|
240
|
+
|
228
241
|
def log(message)
|
229
242
|
@server.log(message)
|
230
243
|
end
|