theme-check 0.8.2 → 0.10.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 +45 -0
- data/CONTRIBUTING.md +20 -90
- data/README.md +4 -1
- data/RELEASING.md +5 -3
- data/Rakefile +31 -0
- data/config/default.yml +45 -0
- data/docs/api/check.md +15 -0
- data/docs/api/html_check.md +46 -0
- data/docs/api/json_check.md +19 -0
- data/docs/api/liquid_check.md +99 -0
- data/docs/checks/{CHECK_DOCS_TEMPLATE.md → TEMPLATE.md.erb} +5 -5
- 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/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/bug.rb +1 -0
- data/lib/theme_check/check.rb +32 -7
- data/lib/theme_check/checks.rb +9 -1
- data/lib/theme_check/checks/TEMPLATE.rb.erb +11 -0
- 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/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/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 +13 -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 +66 -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 +1 -4
- data/lib/theme_check/offense.rb +18 -0
- data/lib/theme_check/template.rb +9 -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 +20 -3
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)
|
@@ -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
|
@@ -38,6 +38,19 @@ module ThemeCheck
|
|
38
38
|
relative_path.sub_ext('').to_s
|
39
39
|
end
|
40
40
|
|
41
|
+
def json?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def liquid?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(other)
|
50
|
+
other.is_a?(JsonFile) && relative_path == other.relative_path
|
51
|
+
end
|
52
|
+
alias_method :eql?, :==
|
53
|
+
|
41
54
|
private
|
42
55
|
|
43
56
|
def load!
|
@@ -9,6 +9,7 @@ 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
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module ThemeCheck
|
5
|
+
module LanguageServer
|
6
|
+
class DiagnosticsTracker
|
7
|
+
def initialize
|
8
|
+
@previously_reported_files = Set.new
|
9
|
+
@single_files_offenses = {}
|
10
|
+
@first_run = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def first_run?
|
14
|
+
@first_run
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_diagnostics(offenses, analyzed_files: nil)
|
18
|
+
reported_files = Set.new
|
19
|
+
new_single_file_offenses = {}
|
20
|
+
analyzed_files = analyzed_files.map { |path| Pathname.new(path) } if analyzed_files
|
21
|
+
|
22
|
+
offenses.group_by(&:template).each do |template, template_offenses|
|
23
|
+
next unless template
|
24
|
+
reported_offenses = template_offenses
|
25
|
+
previous_offenses = @single_files_offenses[template.path]
|
26
|
+
if analyzed_files.nil? || analyzed_files.include?(template.path)
|
27
|
+
# We re-analyzed the file, so we know the template_offenses are update to date.
|
28
|
+
reported_single_file_offenses = reported_offenses.select(&:single_file?)
|
29
|
+
if reported_single_file_offenses.any?
|
30
|
+
new_single_file_offenses[template.path] = reported_single_file_offenses
|
31
|
+
end
|
32
|
+
elsif previous_offenses
|
33
|
+
# Merge in the previous ones, if some
|
34
|
+
reported_offenses |= previous_offenses
|
35
|
+
end
|
36
|
+
yield template.path, reported_offenses
|
37
|
+
reported_files << template.path
|
38
|
+
end
|
39
|
+
|
40
|
+
@single_files_offenses.each do |path, _|
|
41
|
+
# Already reported above, skip
|
42
|
+
next if reported_files.include?(path)
|
43
|
+
|
44
|
+
if analyzed_files.nil? || analyzed_files.include?(path)
|
45
|
+
# We re-analyzed this file, if it was not reported, all offenses in it got fixed
|
46
|
+
yield path, []
|
47
|
+
new_single_file_offenses[path] = nil
|
48
|
+
end
|
49
|
+
# NOTE: No need to re-report previous offenses as LSP should keep them around until
|
50
|
+
# we clear them.
|
51
|
+
reported_files << path
|
52
|
+
end
|
53
|
+
|
54
|
+
# Publish diagnostics with empty array if all issues on a previously reported template
|
55
|
+
# have been fixed.
|
56
|
+
(@previously_reported_files - reported_files).each do |path|
|
57
|
+
yield path, []
|
58
|
+
end
|
59
|
+
|
60
|
+
@previously_reported_files = reported_files
|
61
|
+
@single_files_offenses.merge!(new_single_file_offenses)
|
62
|
+
@first_run = false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "benchmark"
|
2
3
|
|
3
4
|
module ThemeCheck
|
4
5
|
module LanguageServer
|
@@ -19,7 +20,7 @@ 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)
|
@@ -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))
|
@@ -124,17 +126,32 @@ module ThemeCheck
|
|
124
126
|
ignored_patterns: config.ignored_patterns
|
125
127
|
)
|
126
128
|
theme = ThemeCheck::Theme.new(storage)
|
127
|
-
|
128
|
-
offenses = analyze(theme, config)
|
129
|
-
log("Found #{theme.all.size} templates, and #{offenses.size} offenses")
|
130
|
-
send_diagnostics(offenses)
|
131
|
-
end
|
132
|
-
|
133
|
-
def analyze(theme, config)
|
134
129
|
analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
138
155
|
end
|
139
156
|
|
140
157
|
def completions(relative_path, line, col)
|
@@ -145,22 +162,10 @@ module ThemeCheck
|
|
145
162
|
@document_link_engine.document_links(relative_path)
|
146
163
|
end
|
147
164
|
|
148
|
-
def send_diagnostics(offenses)
|
149
|
-
|
150
|
-
|
151
|
-
offenses.group_by(&:template).each do |template, template_offenses|
|
152
|
-
next unless template
|
153
|
-
send_diagnostic(template.path, template_offenses)
|
154
|
-
reported_files << template.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)
|
155
168
|
end
|
156
|
-
|
157
|
-
# Publish diagnostics with empty array if all issues on a previously reported template
|
158
|
-
# have been solved.
|
159
|
-
(@previously_reported_files - reported_files).each do |path|
|
160
|
-
send_diagnostic(path, [])
|
161
|
-
end
|
162
|
-
|
163
|
-
@previously_reported_files = reported_files
|
164
169
|
end
|
165
170
|
|
166
171
|
def send_diagnostic(path, offenses)
|
@@ -52,7 +52,7 @@ module ThemeCheck
|
|
52
52
|
response_body = JSON.dump(response)
|
53
53
|
log(JSON.pretty_generate(response)) if $DEBUG
|
54
54
|
|
55
|
-
@out.write("Content-Length: #{response_body.
|
55
|
+
@out.write("Content-Length: #{response_body.bytesize}\r\n")
|
56
56
|
@out.write("\r\n")
|
57
57
|
@out.write(response_body)
|
58
58
|
@out.flush
|
@@ -6,6 +6,7 @@ module ThemeCheck
|
|
6
6
|
extend ChecksTracking
|
7
7
|
include ParsingHelpers
|
8
8
|
|
9
|
+
# TODO: remove this once all regex checks are migrate to HtmlCheck# TODO: remove this once all regex checks are migrate to HtmlCheck
|
9
10
|
TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
|
10
11
|
VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
|
11
12
|
START_OR_END_QUOTE = /(^['"])|(['"]$)/
|
@@ -16,9 +17,5 @@ module ThemeCheck
|
|
16
17
|
ATTR = /[a-z0-9-]+/i
|
17
18
|
HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
|
18
19
|
HTML_ATTRIBUTES = /(?:#{HTML_ATTRIBUTE}|\s)*/omix
|
19
|
-
|
20
|
-
def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
|
21
|
-
offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
|
22
|
-
end
|
23
20
|
end
|
24
21
|
end
|