theme-check 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +3 -0
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +2 -1
  5. data/README.md +4 -1
  6. data/config/default.yml +37 -0
  7. data/docs/checks/content_for_header_modification.md +42 -0
  8. data/docs/checks/parser_blocking_script_tag.md +53 -0
  9. data/exe/theme-check-language-server +1 -2
  10. data/lib/theme_check.rb +8 -1
  11. data/lib/theme_check/analyzer.rb +72 -16
  12. data/lib/theme_check/check.rb +31 -6
  13. data/lib/theme_check/checks.rb +9 -1
  14. data/lib/theme_check/checks/content_for_header_modification.rb +41 -0
  15. data/lib/theme_check/checks/img_width_and_height.rb +18 -49
  16. data/lib/theme_check/checks/missing_template.rb +1 -0
  17. data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -38
  18. data/lib/theme_check/checks/parser_blocking_script_tag.rb +20 -0
  19. data/lib/theme_check/checks/template_length.rb +3 -0
  20. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  21. data/lib/theme_check/config.rb +2 -0
  22. data/lib/theme_check/disabled_check.rb +6 -4
  23. data/lib/theme_check/disabled_checks.rb +25 -9
  24. data/lib/theme_check/html_check.rb +7 -0
  25. data/lib/theme_check/html_node.rb +52 -0
  26. data/lib/theme_check/html_visitor.rb +36 -0
  27. data/lib/theme_check/json_file.rb +8 -0
  28. data/lib/theme_check/language_server.rb +1 -0
  29. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -0
  30. data/lib/theme_check/language_server/diagnostics_tracker.rb +64 -0
  31. data/lib/theme_check/language_server/handler.rb +31 -26
  32. data/lib/theme_check/language_server/server.rb +1 -1
  33. data/lib/theme_check/liquid_check.rb +0 -4
  34. data/lib/theme_check/offense.rb +18 -0
  35. data/lib/theme_check/template.rb +8 -0
  36. data/lib/theme_check/theme.rb +7 -2
  37. data/lib/theme_check/version.rb +1 -1
  38. data/lib/theme_check/visitor.rb +2 -11
  39. metadata +10 -2
@@ -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
@@ -38,6 +38,14 @@ 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
+
41
49
  private
42
50
 
43
51
  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
@@ -37,6 +37,7 @@ module ThemeCheck
37
37
  partial_match = matches(content, NAMED_FILTER).find do |match|
38
38
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
39
39
  end
40
+ return '' if partial_match.nil?
40
41
  partial_match[1]
41
42
  end
42
43
 
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DiagnosticsTracker
6
+ def initialize
7
+ @previously_reported_files = Set.new
8
+ @single_files_offenses = {}
9
+ @first_run = true
10
+ end
11
+
12
+ def first_run?
13
+ @first_run
14
+ end
15
+
16
+ def build_diagnostics(offenses, analyzed_files: nil)
17
+ reported_files = Set.new
18
+ new_single_file_offenses = {}
19
+
20
+ offenses.group_by(&:template).each do |template, template_offenses|
21
+ next unless template
22
+ reported_offenses = template_offenses
23
+ previous_offenses = @single_files_offenses[template.path]
24
+ if analyzed_files.nil? || analyzed_files.include?(template.path)
25
+ # We re-analyzed the file, so we know the template_offenses are update to date.
26
+ reported_single_file_offenses = reported_offenses.select(&:single_file?)
27
+ if reported_single_file_offenses.any?
28
+ new_single_file_offenses[template.path] = reported_single_file_offenses
29
+ end
30
+ elsif previous_offenses
31
+ # Merge in the previous ones, if some
32
+ reported_offenses |= previous_offenses
33
+ end
34
+ yield template.path, reported_offenses
35
+ reported_files << template.path
36
+ end
37
+
38
+ @single_files_offenses.each do |path, _|
39
+ # Already reported above, skip
40
+ next if reported_files.include?(path)
41
+
42
+ if analyzed_files.nil? || analyzed_files.include?(path)
43
+ # We re-analyzed this file, if it was not reported, all offenses in it got fixed
44
+ yield path, []
45
+ new_single_file_offenses[path] = nil
46
+ end
47
+ # NOTE: No need to re-report previous offenses as LSP should keep them around until
48
+ # we clear them.
49
+ reported_files << path
50
+ end
51
+
52
+ # Publish diagnostics with empty array if all issues on a previously reported template
53
+ # have been fixed.
54
+ (@previously_reported_files - reported_files).each do |path|
55
+ yield path, []
56
+ end
57
+
58
+ @previously_reported_files = reported_files
59
+ @single_files_offenses.merge!(new_single_file_offenses)
60
+ @first_run = false
61
+ end
62
+ end
63
+ end
64
+ 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
- @previously_reported_files = Set.new
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
- log("Checking #{config.root}")
136
- analyzer.analyze_theme
137
- analyzer.offenses
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
- reported_files = Set.new
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.size}\r\n")
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
@@ -16,9 +16,5 @@ module ThemeCheck
16
16
  ATTR = /[a-z0-9-]+/i
17
17
  HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
18
18
  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
19
  end
24
20
  end
@@ -114,6 +114,24 @@ module ThemeCheck
114
114
  end
115
115
  end
116
116
 
117
+ def whole_theme?
118
+ check.whole_theme?
119
+ end
120
+
121
+ def single_file?
122
+ check.single_file?
123
+ end
124
+
125
+ def ==(other)
126
+ other.is_a?(Offense) &&
127
+ check == other.check &&
128
+ message == other.message &&
129
+ template == other.template &&
130
+ node == other.node &&
131
+ markup == other.markup &&
132
+ line_number == other.line_number
133
+ end
134
+
117
135
  def to_s
118
136
  if template
119
137
  "#{message} at #{location}"
@@ -32,6 +32,14 @@ module ThemeCheck
32
32
  relative_path.sub_ext('').to_s
33
33
  end
34
34
 
35
+ def json?
36
+ false
37
+ end
38
+
39
+ def liquid?
40
+ true
41
+ end
42
+
35
43
  def template?
36
44
  name.start_with?('templates')
37
45
  end
@@ -52,8 +52,13 @@ module ThemeCheck
52
52
  @all ||= json + liquid + assets
53
53
  end
54
54
 
55
- def [](name)
56
- all.find { |t| t.name == name }
55
+ def [](name_or_relative_path)
56
+ case name_or_relative_path
57
+ when Pathname
58
+ all.find { |t| t.relative_path == name_or_relative_path }
59
+ else
60
+ all.find { |t| t.name == name_or_relative_path }
61
+ end
57
62
  end
58
63
 
59
64
  def templates
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "0.8.3"
3
+ VERSION = "0.9.0"
4
4
  end
@@ -3,14 +3,13 @@ module ThemeCheck
3
3
  class Visitor
4
4
  attr_reader :checks
5
5
 
6
- def initialize(checks)
6
+ def initialize(checks, disabled_checks)
7
7
  @checks = checks
8
+ @disabled_checks = disabled_checks
8
9
  end
9
10
 
10
11
  def visit_template(template)
11
- @disabled_checks = DisabledChecks.new
12
12
  visit(Node.new(template.root, nil, template))
13
- remove_disabled_offenses
14
13
  rescue Liquid::Error => exception
15
14
  exception.template_name = template.name
16
15
  call_checks(:on_error, exception)
@@ -35,13 +34,5 @@ module ThemeCheck
35
34
  def call_checks(method, *args)
36
35
  checks.call(method, *args)
37
36
  end
38
-
39
- def remove_disabled_offenses
40
- checks.disableable.each do |check|
41
- check.offenses.reject! do |offense|
42
- @disabled_checks.disabled?(offense.code_name, offense.start_index)
43
- end
44
- end
45
- end
46
37
  end
47
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-17 00:00:00.000000000 Z
11
+ date: 2021-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -72,6 +72,7 @@ files:
72
72
  - docs/checks/CHECK_DOCS_TEMPLATE.md
73
73
  - docs/checks/asset_size_css.md
74
74
  - docs/checks/asset_size_javascript.md
75
+ - docs/checks/content_for_header_modification.md
75
76
  - docs/checks/convert_include_to_render.md
76
77
  - docs/checks/default_locale.md
77
78
  - docs/checks/deprecated_filter.md
@@ -84,6 +85,7 @@ files:
84
85
  - docs/checks/missing_template.md
85
86
  - docs/checks/nested_snippet.md
86
87
  - docs/checks/parser_blocking_javascript.md
88
+ - docs/checks/parser_blocking_script_tag.md
87
89
  - docs/checks/remote_asset.md
88
90
  - docs/checks/required_directories.md
89
91
  - docs/checks/required_layout_theme_object.md
@@ -109,6 +111,7 @@ files:
109
111
  - lib/theme_check/checks.rb
110
112
  - lib/theme_check/checks/asset_size_css.rb
111
113
  - lib/theme_check/checks/asset_size_javascript.rb
114
+ - lib/theme_check/checks/content_for_header_modification.rb
112
115
  - lib/theme_check/checks/convert_include_to_render.rb
113
116
  - lib/theme_check/checks/default_locale.rb
114
117
  - lib/theme_check/checks/deprecated_filter.rb
@@ -121,6 +124,7 @@ files:
121
124
  - lib/theme_check/checks/missing_template.rb
122
125
  - lib/theme_check/checks/nested_snippet.rb
123
126
  - lib/theme_check/checks/parser_blocking_javascript.rb
127
+ - lib/theme_check/checks/parser_blocking_script_tag.rb
124
128
  - lib/theme_check/checks/remote_asset.rb
125
129
  - lib/theme_check/checks/required_directories.rb
126
130
  - lib/theme_check/checks/required_layout_theme_object.rb
@@ -143,6 +147,9 @@ files:
143
147
  - lib/theme_check/disabled_checks.rb
144
148
  - lib/theme_check/exceptions.rb
145
149
  - lib/theme_check/file_system_storage.rb
150
+ - lib/theme_check/html_check.rb
151
+ - lib/theme_check/html_node.rb
152
+ - lib/theme_check/html_visitor.rb
146
153
  - lib/theme_check/in_memory_storage.rb
147
154
  - lib/theme_check/json_check.rb
148
155
  - lib/theme_check/json_file.rb
@@ -156,6 +163,7 @@ files:
156
163
  - lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb
157
164
  - lib/theme_check/language_server/completion_providers/tag_completion_provider.rb
158
165
  - lib/theme_check/language_server/constants.rb
166
+ - lib/theme_check/language_server/diagnostics_tracker.rb
159
167
  - lib/theme_check/language_server/document_link_engine.rb
160
168
  - lib/theme_check/language_server/handler.rb
161
169
  - lib/theme_check/language_server/protocol.rb