theme-check 0.8.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 +3 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +4 -1
- data/config/default.yml +37 -0
- data/docs/checks/content_for_header_modification.md +42 -0
- data/docs/checks/parser_blocking_script_tag.md +53 -0
- data/exe/theme-check-language-server +1 -2
- data/lib/theme_check.rb +8 -1
- data/lib/theme_check/analyzer.rb +72 -16
- data/lib/theme_check/check.rb +31 -6
- data/lib/theme_check/checks.rb +9 -1
- 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_template.rb +1 -0
- 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/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 +52 -0
- data/lib/theme_check/html_visitor.rb +36 -0
- data/lib/theme_check/json_file.rb +8 -0
- data/lib/theme_check/language_server.rb +1 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
- 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/liquid_check.rb +0 -4
- data/lib/theme_check/offense.rb +18 -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 +10 -2
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
@@ -13,7 +13,15 @@ module ThemeCheck
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def disableable
|
16
|
-
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?))
|
17
25
|
end
|
18
26
|
|
19
27
|
private
|
@@ -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
|
@@ -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
|
data/lib/theme_check/config.rb
CHANGED
@@ -91,11 +91,13 @@ module ThemeCheck
|
|
91
91
|
|
92
92
|
options_for_check = options.transform_keys(&:to_sym)
|
93
93
|
options_for_check.delete(:enabled)
|
94
|
+
ignored_patterns = options_for_check.delete(:ignore) || []
|
94
95
|
check = if options_for_check.empty?
|
95
96
|
check_class.new
|
96
97
|
else
|
97
98
|
check_class.new(**options_for_check)
|
98
99
|
end
|
100
|
+
check.ignored_patterns = ignored_patterns
|
99
101
|
check.options = options_for_check
|
100
102
|
check
|
101
103
|
end.compact
|
@@ -4,10 +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, :ranges
|
7
|
+
attr_reader :name, :template, :ranges
|
8
8
|
attr_accessor :first_line
|
9
9
|
|
10
|
-
def initialize(name)
|
10
|
+
def initialize(template, name)
|
11
|
+
@template = template
|
11
12
|
@name = name
|
12
13
|
@ranges = []
|
13
14
|
@first_line = false
|
@@ -24,7 +25,8 @@ module ThemeCheck
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def disabled?(index)
|
27
|
-
|
28
|
+
index == 0 && first_line ||
|
29
|
+
ranges.any? { |range| range.cover?(index) }
|
28
30
|
end
|
29
31
|
|
30
32
|
def last
|
@@ -33,7 +35,7 @@ module ThemeCheck
|
|
33
35
|
|
34
36
|
def missing_end_index?
|
35
37
|
return false if first_line && ranges.size == 1
|
36
|
-
last
|
38
|
+
last&.end.nil?
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
@@ -10,28 +10,36 @@ module ThemeCheck
|
|
10
10
|
ACTION_ENABLE_CHECKS = :enable
|
11
11
|
|
12
12
|
def initialize
|
13
|
-
@disabled_checks =
|
13
|
+
@disabled_checks = Hash.new do |hash, key|
|
14
|
+
template, check_name = key
|
15
|
+
hash[key] = DisabledCheck.new(template, check_name)
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
19
|
def update(node)
|
17
20
|
text = comment_text(node)
|
18
21
|
if start_disabling?(text)
|
19
22
|
checks_from_text(text).each do |check_name|
|
20
|
-
@disabled_checks[check_name]
|
21
|
-
|
22
|
-
|
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
|
23
26
|
end
|
24
27
|
elsif stop_disabling?(text)
|
25
28
|
checks_from_text(text).each do |check_name|
|
26
|
-
|
27
|
-
|
29
|
+
disabled = @disabled_checks[[node.template, check_name]]
|
30
|
+
next unless disabled
|
31
|
+
disabled.end_index = node.end_index
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
32
|
-
def disabled?(
|
33
|
-
|
34
|
-
|
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
|
40
|
+
|
41
|
+
@disabled_checks[[template, :all]]&.disabled?(index) ||
|
42
|
+
@disabled_checks[[template, check_name]]&.disabled?(index)
|
35
43
|
end
|
36
44
|
|
37
45
|
def checks_missing_end_index
|
@@ -40,6 +48,14 @@ module ThemeCheck
|
|
40
48
|
.map(&:name)
|
41
49
|
end
|
42
50
|
|
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
|
57
|
+
end
|
58
|
+
|
43
59
|
private
|
44
60
|
|
45
61
|
def comment_text(node)
|