theme-check 0.7.3 → 0.9.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/workflows/theme-check.yml +4 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +43 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/config/default.yml +38 -1
- data/data/shopify_liquid/tags.yml +3 -0
- data/data/shopify_translation_keys.yml +1 -0
- data/dev.yml +1 -1
- 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/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/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/cli.rb +1 -1
- data/lib/theme_check/config.rb +8 -2
- 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 +52 -0
- data/lib/theme_check/html_visitor.rb +36 -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 +0 -4
- data/lib/theme_check/node.rb +12 -0
- data/lib/theme_check/offense.rb +30 -46
- data/lib/theme_check/parsing_helpers.rb +1 -1
- 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
- data/theme-check.gemspec +2 -0
- metadata +18 -5
- data/lib/theme_check/language_server/position_helper.rb +0 -27
|
@@ -17,13 +17,13 @@ module ThemeCheck
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def after_document(node)
|
|
20
|
-
|
|
21
|
-
return
|
|
20
|
+
checks_missing_end_index = @disabled_checks.checks_missing_end_index
|
|
21
|
+
return if checks_missing_end_index.empty?
|
|
22
22
|
|
|
23
|
-
message = if
|
|
23
|
+
message = if checks_missing_end_index.any? { |name| name == :all }
|
|
24
24
|
"All checks were"
|
|
25
25
|
else
|
|
26
|
-
|
|
26
|
+
checks_missing_end_index.join(', ') + " " + (checks_missing_end_index.size == 1 ? "was" : "were")
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
add_offense("#{message} disabled but not re-enabled with theme-check-enable", node: node)
|
|
@@ -1,48 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
module ThemeCheck
|
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
|
4
|
-
class ParserBlockingJavaScript <
|
|
5
|
-
include RegexHelpers
|
|
4
|
+
class ParserBlockingJavaScript < HtmlCheck
|
|
6
5
|
severity :error
|
|
7
|
-
categories :
|
|
6
|
+
categories :html, :performance
|
|
8
7
|
doc docs_url(__FILE__)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
(?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
|
|
14
|
-
/?>
|
|
15
|
-
}xim
|
|
16
|
-
SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
|
|
9
|
+
def on_script(node)
|
|
10
|
+
return unless node.attributes["src"]
|
|
11
|
+
return if node.attributes["defer"] || node.attributes["async"] || node.attributes["type"]&.value == "module"
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
@source = node.template.source
|
|
20
|
-
@node = node
|
|
21
|
-
record_offenses
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def record_offenses
|
|
27
|
-
record_offenses_from_regex(
|
|
28
|
-
message: "Missing async or defer attribute on script tag",
|
|
29
|
-
regex: PARSER_BLOCKING_SCRIPT_TAG,
|
|
30
|
-
)
|
|
31
|
-
record_offenses_from_regex(
|
|
32
|
-
message: "The script_tag filter is parser-blocking. Use a script tag with the async or defer attribute for better performance",
|
|
33
|
-
regex: SCRIPT_TAG_FILTER,
|
|
34
|
-
)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def record_offenses_from_regex(regex: nil, message: nil)
|
|
38
|
-
matches(@source, regex).each do |match|
|
|
39
|
-
add_offense(
|
|
40
|
-
message,
|
|
41
|
-
node: @node,
|
|
42
|
-
markup: match[0],
|
|
43
|
-
line_number: @source[0...match.begin(0)].count("\n") + 1
|
|
44
|
-
)
|
|
45
|
-
end
|
|
13
|
+
add_offense("Missing async or defer attribute on script tag", node: node)
|
|
46
14
|
end
|
|
47
15
|
end
|
|
48
16
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ThemeCheck
|
|
3
|
+
# Reports errors when trying to use parser-blocking script tags
|
|
4
|
+
class ParserBlockingScriptTag < LiquidCheck
|
|
5
|
+
severity :error
|
|
6
|
+
categories :liquid, :performance
|
|
7
|
+
doc docs_url(__FILE__)
|
|
8
|
+
|
|
9
|
+
def on_variable(node)
|
|
10
|
+
used_filters = node.value.filters.map { |name, *_rest| name }
|
|
11
|
+
if used_filters.include?("script_tag")
|
|
12
|
+
add_offense(
|
|
13
|
+
"The script_tag filter is parser-blocking. Use a script tag with the async or defer " \
|
|
14
|
+
"attribute for better performance",
|
|
15
|
+
node: node
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -15,12 +15,18 @@ module ThemeCheck
|
|
|
15
15
|
return if :assign == node.type_name
|
|
16
16
|
|
|
17
17
|
outside_of_strings(node.markup) do |chunk|
|
|
18
|
-
chunk.scan(/([
|
|
18
|
+
chunk.scan(/([,:|]|==|<>|<=|>=|<|>|!=) +/) do |_match|
|
|
19
19
|
add_offense("Too many spaces after '#{Regexp.last_match(1)}'", node: node, markup: Regexp.last_match(0))
|
|
20
20
|
end
|
|
21
|
-
chunk.scan(/([
|
|
21
|
+
chunk.scan(/([,:|]|==|<>|<=|>=|<\b|>\b|!=)(\S|\z)/) do |_match|
|
|
22
22
|
add_offense("Space missing after '#{Regexp.last_match(1)}'", node: node, markup: Regexp.last_match(0))
|
|
23
23
|
end
|
|
24
|
+
chunk.scan(/ (\||==|<>|<=|>=|<|>|!=)+/) do |_match|
|
|
25
|
+
add_offense("Too many spaces before '#{Regexp.last_match(1)}'", node: node, markup: Regexp.last_match(0))
|
|
26
|
+
end
|
|
27
|
+
chunk.scan(/(\A|\S)(?<match>\||==|<>|<=|>=|<|\b>|!=)/) do |_match|
|
|
28
|
+
add_offense("Space missing before '#{Regexp.last_match(1)}'", node: node, markup: Regexp.last_match(0))
|
|
29
|
+
end
|
|
24
30
|
end
|
|
25
31
|
end
|
|
26
32
|
|
data/lib/theme_check/cli.rb
CHANGED
|
@@ -17,7 +17,7 @@ module ThemeCheck
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def option_parser(parser = OptionParser.new, help: true)
|
|
20
|
-
return @option_parser if @option_parser
|
|
20
|
+
return @option_parser if defined?(@option_parser)
|
|
21
21
|
@option_parser = parser
|
|
22
22
|
@option_parser.banner = "Usage: theme-check [options] [/path/to/your/theme]"
|
|
23
23
|
|
data/lib/theme_check/config.rb
CHANGED
|
@@ -91,7 +91,13 @@ module ThemeCheck
|
|
|
91
91
|
|
|
92
92
|
options_for_check = options.transform_keys(&:to_sym)
|
|
93
93
|
options_for_check.delete(:enabled)
|
|
94
|
-
|
|
94
|
+
ignored_patterns = options_for_check.delete(:ignore) || []
|
|
95
|
+
check = if options_for_check.empty?
|
|
96
|
+
check_class.new
|
|
97
|
+
else
|
|
98
|
+
check_class.new(**options_for_check)
|
|
99
|
+
end
|
|
100
|
+
check.ignored_patterns = ignored_patterns
|
|
95
101
|
check.options = options_for_check
|
|
96
102
|
check
|
|
97
103
|
end.compact
|
|
@@ -104,7 +110,7 @@ module ThemeCheck
|
|
|
104
110
|
private
|
|
105
111
|
|
|
106
112
|
def check_name?(name)
|
|
107
|
-
name.start_with?(/[A-Z]/)
|
|
113
|
+
name.to_s.start_with?(/[A-Z]/)
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def validate_configuration(configuration, default_configuration = self.class.default, parent_keys = [])
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This class keeps track of checks being turned on and off in ranges.
|
|
4
|
+
# We'll use the node position to figure out if the test is disabled or not.
|
|
5
|
+
module ThemeCheck
|
|
6
|
+
class DisabledCheck
|
|
7
|
+
attr_reader :name, :template, :ranges
|
|
8
|
+
attr_accessor :first_line
|
|
9
|
+
|
|
10
|
+
def initialize(template, name)
|
|
11
|
+
@template = template
|
|
12
|
+
@name = name
|
|
13
|
+
@ranges = []
|
|
14
|
+
@first_line = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start_index=(index)
|
|
18
|
+
return unless ranges.empty? || !last.end.nil?
|
|
19
|
+
@ranges << (index..)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def end_index=(index)
|
|
23
|
+
return if ranges.empty? || !last.end.nil?
|
|
24
|
+
@ranges << (@ranges.pop.begin..index)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def disabled?(index)
|
|
28
|
+
index == 0 && first_line ||
|
|
29
|
+
ranges.any? { |range| range.cover?(index) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def last
|
|
33
|
+
ranges.last
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def missing_end_index?
|
|
37
|
+
return false if first_line && ranges.size == 1
|
|
38
|
+
last&.end.nil?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -8,50 +8,52 @@ module ThemeCheck
|
|
|
8
8
|
|
|
9
9
|
ACTION_DISABLE_CHECKS = :disable
|
|
10
10
|
ACTION_ENABLE_CHECKS = :enable
|
|
11
|
-
ACTION_UNRELATED_COMMENT = :unrelated
|
|
12
11
|
|
|
13
12
|
def initialize
|
|
14
|
-
@
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
@disabled_checks = Hash.new do |hash, key|
|
|
14
|
+
template, check_name = key
|
|
15
|
+
hash[key] = DisabledCheck.new(template, check_name)
|
|
16
|
+
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def update(node)
|
|
20
20
|
text = comment_text(node)
|
|
21
|
-
|
|
22
21
|
if start_disabling?(text)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@full_document_disabled = true
|
|
22
|
+
checks_from_text(text).each do |check_name|
|
|
23
|
+
disabled = @disabled_checks[[node.template, check_name]]
|
|
24
|
+
disabled.start_index = node.start_index
|
|
25
|
+
disabled.first_line = true if node.line_number == 1
|
|
28
26
|
end
|
|
29
27
|
elsif stop_disabling?(text)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
checks_from_text(text).each do |check_name|
|
|
29
|
+
disabled = @disabled_checks[[node.template, check_name]]
|
|
30
|
+
next unless disabled
|
|
31
|
+
disabled.end_index = node.end_index
|
|
32
|
+
end
|
|
34
33
|
end
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
def disabled?(check, template, check_name, index)
|
|
37
|
+
return true if check.ignored_patterns&.any? do |pattern|
|
|
38
|
+
template.relative_path.fnmatch?(pattern)
|
|
39
|
+
end
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@all_disabled
|
|
41
|
+
@disabled_checks[[template, :all]]&.disabled?(index) ||
|
|
42
|
+
@disabled_checks[[template, check_name]]&.disabled?(index)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
def checks_missing_end_index
|
|
46
|
+
@disabled_checks.values
|
|
47
|
+
.select(&:missing_end_index?)
|
|
48
|
+
.map(&:name)
|
|
50
49
|
end
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
def remove_disabled_offenses(checks)
|
|
52
|
+
checks.disableable.each do |check|
|
|
53
|
+
check.offenses.reject! do |offense|
|
|
54
|
+
disabled?(check, offense.template, offense.code_name, offense.start_index)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
private
|
|
@@ -69,9 +71,11 @@ module ThemeCheck
|
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
# Return a list of checks from a theme-check-disable comment
|
|
72
|
-
# Returns [] if all checks are meant to be disabled
|
|
74
|
+
# Returns [:all] if all checks are meant to be disabled
|
|
73
75
|
def checks_from_text(text)
|
|
74
|
-
text.gsub(DISABLE_PREFIX_PATTERN, '').strip.split(',').map(&:strip)
|
|
76
|
+
checks = text.gsub(DISABLE_PREFIX_PATTERN, '').strip.split(',').map(&:strip)
|
|
77
|
+
return [:all] if checks.empty?
|
|
78
|
+
checks
|
|
75
79
|
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "net/http"
|
|
3
|
+
|
|
4
|
+
TIMEOUT_EXCEPTIONS = [
|
|
5
|
+
Net::ReadTimeout,
|
|
6
|
+
Net::OpenTimeout,
|
|
7
|
+
Net::WriteTimeout,
|
|
8
|
+
Errno::ETIMEDOUT,
|
|
9
|
+
Timeout::Error,
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
CONNECTION_EXCEPTIONS = [
|
|
13
|
+
IOError,
|
|
14
|
+
EOFError,
|
|
15
|
+
SocketError,
|
|
16
|
+
Errno::EINVAL,
|
|
17
|
+
Errno::ECONNRESET,
|
|
18
|
+
Errno::ECONNABORTED,
|
|
19
|
+
Errno::EPIPE,
|
|
20
|
+
Errno::ECONNREFUSED,
|
|
21
|
+
Errno::EAGAIN,
|
|
22
|
+
Errno::EHOSTUNREACH,
|
|
23
|
+
Errno::ENETUNREACH,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
NET_HTTP_EXCEPTIONS = [
|
|
27
|
+
Net::HTTPBadResponse,
|
|
28
|
+
Net::HTTPHeaderSyntaxError,
|
|
29
|
+
Net::ProtocolError,
|
|
30
|
+
*TIMEOUT_EXCEPTIONS,
|
|
31
|
+
*CONNECTION_EXCEPTIONS,
|
|
32
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
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 children
|
|
21
|
+
@value.children.map { |child| HtmlNode.new(child, template) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parent
|
|
25
|
+
HtmlNode.new(@value.parent, template)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
if @value.name == "#document-fragment"
|
|
30
|
+
"document"
|
|
31
|
+
else
|
|
32
|
+
@value.name
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def value
|
|
37
|
+
if literal?
|
|
38
|
+
@value.content
|
|
39
|
+
else
|
|
40
|
+
@value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def markup
|
|
45
|
+
@value.to_html
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def line_number
|
|
49
|
+
@value.line
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
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_#{node.name}", node)
|
|
26
|
+
node.children.each { |child| visit(child) }
|
|
27
|
+
unless node.literal?
|
|
28
|
+
call_checks(:"after_#{node.name}", node)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call_checks(method, *args)
|
|
33
|
+
checks.call(method, *args)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|