theme-check 0.7.3 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/theme_check.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "liquid"
|
3
3
|
|
4
|
+
require_relative "theme_check/version"
|
5
|
+
require_relative "theme_check/bug"
|
6
|
+
require_relative "theme_check/exceptions"
|
4
7
|
require_relative "theme_check/analyzer"
|
5
8
|
require_relative "theme_check/check"
|
6
9
|
require_relative "theme_check/checks_tracking"
|
7
10
|
require_relative "theme_check/cli"
|
11
|
+
require_relative "theme_check/disabled_check"
|
8
12
|
require_relative "theme_check/disabled_checks"
|
9
13
|
require_relative "theme_check/liquid_check"
|
10
14
|
require_relative "theme_check/locale_diff"
|
@@ -14,6 +18,8 @@ require_relative "theme_check/regex_helpers"
|
|
14
18
|
require_relative "theme_check/json_check"
|
15
19
|
require_relative "theme_check/json_file"
|
16
20
|
require_relative "theme_check/json_helpers"
|
21
|
+
require_relative "theme_check/position_helper"
|
22
|
+
require_relative "theme_check/position"
|
17
23
|
require_relative "theme_check/language_server"
|
18
24
|
require_relative "theme_check/checks"
|
19
25
|
require_relative "theme_check/config"
|
@@ -30,6 +36,12 @@ require_relative "theme_check/template"
|
|
30
36
|
require_relative "theme_check/theme"
|
31
37
|
require_relative "theme_check/visitor"
|
32
38
|
require_relative "theme_check/corrector"
|
33
|
-
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"
|
34
42
|
|
35
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
@@ -1,53 +1,119 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
3
|
class Analyzer
|
4
|
-
attr_reader :offenses
|
5
|
-
|
6
4
|
def initialize(theme, checks = Check.all.map(&:new), auto_correct = false)
|
7
5
|
@theme = theme
|
8
|
-
@offenses = []
|
9
6
|
@auto_correct = auto_correct
|
10
7
|
|
11
8
|
@liquid_checks = Checks.new
|
12
9
|
@json_checks = Checks.new
|
10
|
+
@html_checks = Checks.new
|
13
11
|
|
14
12
|
checks.each do |check|
|
15
13
|
check.theme = @theme
|
16
|
-
check.offenses = @offenses
|
17
14
|
|
18
15
|
case check
|
19
16
|
when LiquidCheck
|
20
17
|
@liquid_checks << check
|
21
18
|
when JsonCheck
|
22
19
|
@json_checks << check
|
20
|
+
when HtmlCheck
|
21
|
+
@html_checks << check
|
23
22
|
end
|
24
23
|
end
|
24
|
+
end
|
25
25
|
|
26
|
-
|
26
|
+
def 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
32
|
def analyze_theme
|
30
|
-
|
31
|
-
|
33
|
+
reset
|
34
|
+
|
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)
|
40
|
+
end
|
41
|
+
|
32
42
|
@theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
36
72
|
end
|
37
73
|
|
38
74
|
def uncorrectable_offenses
|
39
75
|
unless @auto_correct
|
40
|
-
return
|
76
|
+
return offenses
|
41
77
|
end
|
42
78
|
|
43
|
-
|
79
|
+
offenses.select { |offense| !offense.correctable? }
|
44
80
|
end
|
45
81
|
|
46
82
|
def correct_offenses
|
47
83
|
if @auto_correct
|
48
|
-
|
84
|
+
offenses.each(&:correct)
|
49
85
|
@theme.liquid.each(&:write)
|
50
86
|
end
|
51
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
|
52
118
|
end
|
53
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,8 +6,8 @@ module ThemeCheck
|
|
6
6
|
include JsonHelpers
|
7
7
|
|
8
8
|
attr_accessor :theme
|
9
|
-
attr_accessor :
|
10
|
-
|
9
|
+
attr_accessor :options, :ignored_patterns
|
10
|
+
attr_writer :offenses
|
11
11
|
|
12
12
|
SEVERITIES = [
|
13
13
|
:error,
|
@@ -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,6 +68,21 @@ 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
|
78
|
+
end
|
79
|
+
|
80
|
+
def offenses
|
81
|
+
@offenses ||= []
|
82
|
+
end
|
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)
|
70
86
|
end
|
71
87
|
|
72
88
|
def severity
|
@@ -89,10 +105,6 @@ module ThemeCheck
|
|
89
105
|
@ignored = true
|
90
106
|
end
|
91
107
|
|
92
|
-
def unignore!
|
93
|
-
@ignored = false
|
94
|
-
end
|
95
|
-
|
96
108
|
def ignored?
|
97
109
|
defined?(@ignored) && @ignored
|
98
110
|
end
|
@@ -101,9 +113,26 @@ module ThemeCheck
|
|
101
113
|
self.class.can_disable
|
102
114
|
end
|
103
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
|
+
|
104
128
|
def to_s
|
105
129
|
s = +"#{code_name}:\n"
|
106
|
-
properties = {
|
130
|
+
properties = {
|
131
|
+
severity: severity,
|
132
|
+
categories: categories,
|
133
|
+
doc: doc,
|
134
|
+
ignored_patterns: ignored_patterns,
|
135
|
+
}.merge(options)
|
107
136
|
properties.each_pair do |name, value|
|
108
137
|
s << " #{name}: #{value}\n" if value
|
109
138
|
end
|
data/lib/theme_check/checks.rb
CHANGED
@@ -1,22 +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
|
-
def
|
13
|
-
self.class.new(
|
15
|
+
def disableable
|
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?))
|
14
25
|
end
|
15
26
|
|
16
|
-
|
17
|
-
|
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
|
+
```
|
18
50
|
|
19
|
-
|
51
|
+
Template: `#{template}`
|
52
|
+
Node: `#{node_class}`
|
53
|
+
Markup:
|
54
|
+
```
|
55
|
+
#{markup}
|
56
|
+
```
|
57
|
+
Check options: `#{check.options.pretty_inspect}`
|
58
|
+
EOS
|
20
59
|
end
|
21
60
|
end
|
22
61
|
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
|
@@ -1,41 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module ThemeCheck
|
3
3
|
# Reports errors when trying to use parser-blocking script tags
|
4
|
-
class ImgWidthAndHeight <
|
5
|
-
include RegexHelpers
|
4
|
+
class ImgWidthAndHeight < HtmlCheck
|
6
5
|
severity :error
|
7
|
-
categories :
|
6
|
+
categories :html, :performance
|
8
7
|
doc docs_url(__FILE__)
|
9
8
|
|
10
|
-
# Not implemented with lookbehinds and lookaheads because performance was shit!
|
11
|
-
IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
|
12
|
-
SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
13
|
-
WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
14
|
-
HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
15
|
-
|
16
|
-
FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
|
17
9
|
ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
|
18
10
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
record_offenses
|
23
|
-
end
|
11
|
+
def on_img(node)
|
12
|
+
width = node.attributes["width"]&.value
|
13
|
+
height = node.attributes["height"]&.value
|
24
14
|
|
25
|
-
|
15
|
+
record_units_in_field_offenses("width", width, node: node)
|
16
|
+
record_units_in_field_offenses("height", height, node: node)
|
26
17
|
|
27
|
-
|
28
|
-
matches(@source, IMG_TAG).each do |img_match|
|
29
|
-
next unless img_match[0] =~ SRC_ATTRIBUTE
|
30
|
-
record_missing_field_offenses(img_match)
|
31
|
-
record_units_in_field_offenses(img_match)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def record_missing_field_offenses(img_match)
|
36
|
-
width = WIDTH_ATTRIBUTE.match(img_match[0])
|
37
|
-
height = HEIGHT_ATTRIBUTE.match(img_match[0])
|
38
|
-
return if width && height
|
18
|
+
return if node.attributes["src"].nil? || (width && height)
|
39
19
|
missing_width = width.nil?
|
40
20
|
missing_height = height.nil?
|
41
21
|
error_message = if missing_width && missing_height
|
@@ -46,29 +26,18 @@ module ThemeCheck
|
|
46
26
|
"Missing height attribute"
|
47
27
|
end
|
48
28
|
|
49
|
-
add_offense(
|
50
|
-
error_message,
|
51
|
-
node: @node,
|
52
|
-
markup: img_match[0],
|
53
|
-
line_number: @source[0...img_match.begin(0)].count("\n") + 1
|
54
|
-
)
|
29
|
+
add_offense(error_message, node: node)
|
55
30
|
end
|
56
31
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
"The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
|
67
|
-
node: @node,
|
68
|
-
markup: value,
|
69
|
-
line_number: @source[0...start].count("\n") + 1
|
70
|
-
)
|
71
|
-
end
|
32
|
+
private
|
33
|
+
|
34
|
+
def record_units_in_field_offenses(attribute, value, node:)
|
35
|
+
return unless value =~ ENDS_IN_CSS_UNIT
|
36
|
+
value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
|
37
|
+
add_offense(
|
38
|
+
"The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\".",
|
39
|
+
node: node,
|
40
|
+
)
|
72
41
|
end
|
73
42
|
end
|
74
43
|
end
|