theme-check 0.8.1 → 0.10.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 +3 -0
- data/CHANGELOG.md +39 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/config/default.yml +46 -1
- data/data/shopify_liquid/tags.yml +3 -0
- data/docs/checks/asset_url_filters.md +56 -0
- data/docs/checks/content_for_header_modification.md +42 -0
- data/docs/checks/img_lazy_loading.md +61 -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 +22 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +9 -1
- data/lib/theme_check/analyzer.rb +72 -16
- data/lib/theme_check/bug.rb +20 -0
- data/lib/theme_check/check.rb +31 -6
- data/lib/theme_check/checks.rb +49 -4
- 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_lazy_loading.rb +25 -0
- data/lib/theme_check/checks/img_width_and_height.rb +18 -49
- 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 +5 -5
- 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 +6 -4
- data/lib/theme_check/disabled_checks.rb +25 -9
- 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 +8 -0
- data/lib/theme_check/language_server.rb +2 -0
- 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/diagnostics_tracker.rb +64 -0
- data/lib/theme_check/language_server/handler.rb +31 -26
- 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/offense.rb +18 -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 +2 -11
- metadata +17 -3
@@ -21,6 +21,10 @@ This check is aimed at eliminating ugly Liquid:
|
|
21
21
|
<!-- Arround filter pipelines -->
|
22
22
|
{{ url | asset_url | img_tag }}
|
23
23
|
{% assign my_upcase_string = "Hello world"| upcase %}
|
24
|
+
|
25
|
+
<!-- Arround symbol operators -->
|
26
|
+
{%- if target == product and product.price_varies -%}
|
27
|
+
{%- if product.featured_media.width >=165 -%}
|
24
28
|
```
|
25
29
|
|
26
30
|
:+1: Examples of **correct** code for this check:
|
@@ -39,6 +43,8 @@ This check is aimed at eliminating ugly Liquid:
|
|
39
43
|
%}
|
40
44
|
{{ url | asset_url | img_tag }}
|
41
45
|
{% assign my_upcase_string = "Hello world" | upcase %}
|
46
|
+
{%- if target == product and product.price_varies -%}
|
47
|
+
{%- if product.featured_media.width >= 165 -%}
|
42
48
|
```
|
43
49
|
|
44
50
|
## Check Options
|
@@ -50,6 +56,22 @@ SpaceInsideBraces:
|
|
50
56
|
enabled: true
|
51
57
|
```
|
52
58
|
|
59
|
+
## Auto-correction
|
60
|
+
|
61
|
+
This check can automatically trim or add spaces around `{{ ... }}`.
|
62
|
+
|
63
|
+
```liquid
|
64
|
+
{{ x}}
|
65
|
+
{{x}}
|
66
|
+
{{ x }}
|
67
|
+
```
|
68
|
+
|
69
|
+
Can all be auto-corrected with the `--auto-correct` option to:
|
70
|
+
|
71
|
+
```liquid
|
72
|
+
{{ x }}
|
73
|
+
```
|
74
|
+
|
53
75
|
## When Not To Use It
|
54
76
|
|
55
77
|
If you don't care about the look of your code.
|
data/lib/theme_check.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "liquid"
|
3
3
|
|
4
|
+
require_relative "theme_check/version"
|
5
|
+
require_relative "theme_check/bug"
|
4
6
|
require_relative "theme_check/exceptions"
|
5
7
|
require_relative "theme_check/analyzer"
|
6
8
|
require_relative "theme_check/check"
|
@@ -34,6 +36,12 @@ require_relative "theme_check/template"
|
|
34
36
|
require_relative "theme_check/theme"
|
35
37
|
require_relative "theme_check/visitor"
|
36
38
|
require_relative "theme_check/corrector"
|
37
|
-
require_relative "theme_check/
|
39
|
+
require_relative "theme_check/html_node"
|
40
|
+
require_relative "theme_check/html_visitor"
|
41
|
+
require_relative "theme_check/html_check"
|
38
42
|
|
39
43
|
Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }
|
44
|
+
|
45
|
+
# UTF-8 is the default internal and external encoding, like in Rails & Shopify.
|
46
|
+
Encoding.default_external = Encoding::UTF_8
|
47
|
+
Encoding.default_internal = Encoding::UTF_8
|
data/lib/theme_check/analyzer.rb
CHANGED
@@ -7,6 +7,7 @@ module ThemeCheck
|
|
7
7
|
|
8
8
|
@liquid_checks = Checks.new
|
9
9
|
@json_checks = Checks.new
|
10
|
+
@html_checks = Checks.new
|
10
11
|
|
11
12
|
checks.each do |check|
|
12
13
|
check.theme = @theme
|
@@ -16,33 +17,58 @@ module ThemeCheck
|
|
16
17
|
@liquid_checks << check
|
17
18
|
when JsonCheck
|
18
19
|
@json_checks << check
|
20
|
+
when HtmlCheck
|
21
|
+
@html_checks << check
|
19
22
|
end
|
20
23
|
end
|
21
|
-
|
22
|
-
@visitor = Visitor.new(@liquid_checks)
|
23
24
|
end
|
24
25
|
|
25
26
|
def offenses
|
26
|
-
@liquid_checks.flat_map(&:offenses) +
|
27
|
+
@liquid_checks.flat_map(&:offenses) +
|
28
|
+
@json_checks.flat_map(&:offenses) +
|
29
|
+
@html_checks.flat_map(&:offenses)
|
27
30
|
end
|
28
31
|
|
29
|
-
def
|
30
|
-
|
31
|
-
check.offenses.clear
|
32
|
-
end
|
32
|
+
def analyze_theme
|
33
|
+
reset
|
33
34
|
|
34
|
-
@
|
35
|
-
|
35
|
+
liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
|
36
|
+
html_visitor = HtmlVisitor.new(@html_checks)
|
37
|
+
@theme.liquid.each do |template|
|
38
|
+
liquid_visitor.visit_template(template)
|
39
|
+
html_visitor.visit_template(template)
|
36
40
|
end
|
37
|
-
end
|
38
41
|
|
39
|
-
def analyze_theme
|
40
|
-
offenses_clear!
|
41
|
-
@theme.liquid.each { |template| @visitor.visit_template(template) }
|
42
42
|
@theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
|
43
|
-
|
44
|
-
|
45
|
-
|
43
|
+
|
44
|
+
finish
|
45
|
+
end
|
46
|
+
|
47
|
+
def analyze_files(files)
|
48
|
+
reset
|
49
|
+
|
50
|
+
# Call all checks that run on the whole theme
|
51
|
+
liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
|
52
|
+
html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
|
53
|
+
@theme.liquid.each do |template|
|
54
|
+
liquid_visitor.visit_template(template)
|
55
|
+
html_visitor.visit_template(template)
|
56
|
+
end
|
57
|
+
@theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
|
58
|
+
|
59
|
+
# Call checks that run on a single files, only on specified file
|
60
|
+
liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
|
61
|
+
html_visitor = HtmlVisitor.new(@html_checks.single_file)
|
62
|
+
files.each do |file|
|
63
|
+
if file.liquid?
|
64
|
+
liquid_visitor.visit_template(file)
|
65
|
+
html_visitor.visit_template(file)
|
66
|
+
elsif file.json?
|
67
|
+
@json_checks.single_file.call(:on_file, file)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
finish
|
46
72
|
end
|
47
73
|
|
48
74
|
def uncorrectable_offenses
|
@@ -59,5 +85,35 @@ module ThemeCheck
|
|
59
85
|
@theme.liquid.each(&:write)
|
60
86
|
end
|
61
87
|
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def reset
|
92
|
+
@disabled_checks = DisabledChecks.new
|
93
|
+
|
94
|
+
@liquid_checks.each do |check|
|
95
|
+
check.offenses.clear
|
96
|
+
end
|
97
|
+
|
98
|
+
@html_checks.each do |check|
|
99
|
+
check.offenses.clear
|
100
|
+
end
|
101
|
+
|
102
|
+
@json_checks.each do |check|
|
103
|
+
check.offenses.clear
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def finish
|
108
|
+
@liquid_checks.call(:on_end)
|
109
|
+
@html_checks.call(:on_end)
|
110
|
+
@json_checks.call(:on_end)
|
111
|
+
|
112
|
+
@disabled_checks.remove_disabled_offenses(@liquid_checks)
|
113
|
+
@disabled_checks.remove_disabled_offenses(@json_checks)
|
114
|
+
@disabled_checks.remove_disabled_offenses(@html_checks)
|
115
|
+
|
116
|
+
offenses
|
117
|
+
end
|
62
118
|
end
|
63
119
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'theme_check/version'
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
BUG_POSTAMBLE = <<~EOS
|
6
|
+
Theme Check Version: #{VERSION}
|
7
|
+
Ruby Version: #{RUBY_VERSION}
|
8
|
+
Platform: #{RUBY_PLATFORM}
|
9
|
+
Muffin mode: activated
|
10
|
+
|
11
|
+
------------------------
|
12
|
+
Whoops! It looks like you found a bug in Theme Check.
|
13
|
+
Please report it at https://github.com/Shopify/theme-check/issues, and include the message above.
|
14
|
+
Or cross your fingers real hard, and try again.
|
15
|
+
EOS
|
16
|
+
|
17
|
+
def self.bug(message)
|
18
|
+
abort(message + BUG_POSTAMBLE)
|
19
|
+
end
|
20
|
+
end
|
data/lib/theme_check/check.rb
CHANGED
@@ -6,7 +6,7 @@ module ThemeCheck
|
|
6
6
|
include JsonHelpers
|
7
7
|
|
8
8
|
attr_accessor :theme
|
9
|
-
attr_accessor :options
|
9
|
+
attr_accessor :options, :ignored_patterns
|
10
10
|
attr_writer :offenses
|
11
11
|
|
12
12
|
SEVERITIES = [
|
@@ -19,6 +19,7 @@ module ThemeCheck
|
|
19
19
|
:liquid,
|
20
20
|
:translation,
|
21
21
|
:performance,
|
22
|
+
:html,
|
22
23
|
:json,
|
23
24
|
:performance,
|
24
25
|
]
|
@@ -67,12 +68,23 @@ module ThemeCheck
|
|
67
68
|
end
|
68
69
|
defined?(@can_disable) ? @can_disable : true
|
69
70
|
end
|
71
|
+
|
72
|
+
def single_file(single_file = nil)
|
73
|
+
unless single_file.nil?
|
74
|
+
@single_file = single_file
|
75
|
+
end
|
76
|
+
defined?(@single_file) ? @single_file : !method_defined?(:on_end)
|
77
|
+
end
|
70
78
|
end
|
71
79
|
|
72
80
|
def offenses
|
73
81
|
@offenses ||= []
|
74
82
|
end
|
75
83
|
|
84
|
+
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
|
85
|
+
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
|
86
|
+
end
|
87
|
+
|
76
88
|
def severity
|
77
89
|
self.class.severity
|
78
90
|
end
|
@@ -93,10 +105,6 @@ module ThemeCheck
|
|
93
105
|
@ignored = true
|
94
106
|
end
|
95
107
|
|
96
|
-
def unignore!
|
97
|
-
@ignored = false
|
98
|
-
end
|
99
|
-
|
100
108
|
def ignored?
|
101
109
|
defined?(@ignored) && @ignored
|
102
110
|
end
|
@@ -105,9 +113,26 @@ module ThemeCheck
|
|
105
113
|
self.class.can_disable
|
106
114
|
end
|
107
115
|
|
116
|
+
def single_file?
|
117
|
+
self.class.single_file
|
118
|
+
end
|
119
|
+
|
120
|
+
def whole_theme?
|
121
|
+
!single_file?
|
122
|
+
end
|
123
|
+
|
124
|
+
def ==(other)
|
125
|
+
other.is_a?(Check) && code_name == other.code_name
|
126
|
+
end
|
127
|
+
|
108
128
|
def to_s
|
109
129
|
s = +"#{code_name}:\n"
|
110
|
-
properties = {
|
130
|
+
properties = {
|
131
|
+
severity: severity,
|
132
|
+
categories: categories,
|
133
|
+
doc: doc,
|
134
|
+
ignored_patterns: ignored_patterns,
|
135
|
+
}.merge(options)
|
111
136
|
properties.each_pair do |name, value|
|
112
137
|
s << " #{name}: #{value}\n" if value
|
113
138
|
end
|
data/lib/theme_check/checks.rb
CHANGED
@@ -1,16 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "pp"
|
3
|
+
require "timeout"
|
4
|
+
|
2
5
|
module ThemeCheck
|
3
6
|
class Checks < Array
|
7
|
+
CHECK_METHOD_TIMEOUT = 5 # sec
|
8
|
+
|
4
9
|
def call(method, *args)
|
5
10
|
each do |check|
|
6
|
-
|
7
|
-
check.send(method, *args)
|
8
|
-
end
|
11
|
+
call_check_method(check, method, *args)
|
9
12
|
end
|
10
13
|
end
|
11
14
|
|
12
15
|
def disableable
|
13
|
-
self.class.new(select(&:can_disable?))
|
16
|
+
@disableable ||= self.class.new(select(&:can_disable?))
|
17
|
+
end
|
18
|
+
|
19
|
+
def whole_theme
|
20
|
+
@whole_theme ||= self.class.new(select(&:whole_theme?))
|
21
|
+
end
|
22
|
+
|
23
|
+
def single_file
|
24
|
+
@single_file ||= self.class.new(select(&:single_file?))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def call_check_method(check, method, *args)
|
30
|
+
return unless check.respond_to?(method) && !check.ignored?
|
31
|
+
|
32
|
+
Timeout.timeout(CHECK_METHOD_TIMEOUT) do
|
33
|
+
check.send(method, *args)
|
34
|
+
end
|
35
|
+
rescue Liquid::Error
|
36
|
+
# Pass-through Liquid errors
|
37
|
+
raise
|
38
|
+
rescue => e
|
39
|
+
node = args.first
|
40
|
+
template = node.respond_to?(:template) ? node.template.relative_path : "?"
|
41
|
+
markup = node.respond_to?(:markup) ? node.markup : ""
|
42
|
+
node_class = node.respond_to?(:value) ? node.value.class : "?"
|
43
|
+
|
44
|
+
ThemeCheck.bug(<<~EOS)
|
45
|
+
Exception while running `#{check.code_name}##{method}`:
|
46
|
+
```
|
47
|
+
#{e.class}: #{e.message}
|
48
|
+
#{e.backtrace.join("\n ")}
|
49
|
+
```
|
50
|
+
|
51
|
+
Template: `#{template}`
|
52
|
+
Node: `#{node_class}`
|
53
|
+
Markup:
|
54
|
+
```
|
55
|
+
#{markup}
|
56
|
+
```
|
57
|
+
Check options: `#{check.options.pretty_inspect}`
|
58
|
+
EOS
|
14
59
|
end
|
15
60
|
end
|
16
61
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class AssetUrlFilters < LiquidCheck
|
4
|
+
severity :suggestion
|
5
|
+
categories :liquid, :performance
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
HTML_FILTERS = [
|
9
|
+
'stylesheet_tag',
|
10
|
+
'script_tag',
|
11
|
+
'img_tag',
|
12
|
+
]
|
13
|
+
ASSET_URL_FILTERS = [
|
14
|
+
'asset_url',
|
15
|
+
'asset_img_url',
|
16
|
+
'file_img_url',
|
17
|
+
'file_url',
|
18
|
+
'global_asset_url',
|
19
|
+
'img_url',
|
20
|
+
'payment_type_img_url',
|
21
|
+
'shopify_asset_url',
|
22
|
+
]
|
23
|
+
|
24
|
+
def on_variable(node)
|
25
|
+
record_variable_offense(node)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def record_variable_offense(variable_node)
|
31
|
+
# We flag HTML tags with URLs not hosted by Shopify
|
32
|
+
return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
|
33
|
+
add_offense("Use one of the asset_url filters to serve assets", node: variable_node)
|
34
|
+
end
|
35
|
+
|
36
|
+
def html_resource_drop?(variable_node)
|
37
|
+
variable_node.value.filters
|
38
|
+
.any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def variable_hosted_by_shopify?(variable_node)
|
42
|
+
variable_node.value.filters
|
43
|
+
.any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class ContentForHeaderModification < LiquidCheck
|
4
|
+
severity :error
|
5
|
+
category :liquid
|
6
|
+
doc docs_url(__FILE__)
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@in_assign = false
|
10
|
+
@in_capture = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_variable(node)
|
14
|
+
return unless node.value.name.is_a?(Liquid::VariableLookup)
|
15
|
+
return unless node.value.name.name == "content_for_header"
|
16
|
+
|
17
|
+
if @in_assign || @in_capture || node.value.filters.any?
|
18
|
+
add_offense(
|
19
|
+
"Do not rely on the content of `content_for_header`",
|
20
|
+
node: node,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_assign(_node)
|
26
|
+
@in_assign = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_assign(_node)
|
30
|
+
@in_assign = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_capture(_node)
|
34
|
+
@in_capture = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_capture(_node)
|
38
|
+
@in_capture = false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|