theme-check 0.10.2 → 1.3.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +2 -6
  3. data/CHANGELOG.md +51 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +39 -0
  6. data/RELEASING.md +34 -2
  7. data/bin/theme-check +29 -0
  8. data/bin/theme-check-language-server +29 -0
  9. data/config/default.yml +46 -3
  10. data/config/nothing.yml +11 -0
  11. data/config/theme_app_extension.yml +168 -0
  12. data/data/shopify_liquid/objects.yml +2 -0
  13. data/docs/checks/app_block_valid_tags.md +40 -0
  14. data/docs/checks/asset_size_app_block_css.md +52 -0
  15. data/docs/checks/asset_size_app_block_javascript.md +57 -0
  16. data/docs/checks/asset_size_css_stylesheet_tag.md +50 -0
  17. data/docs/checks/deprecate_bgsizes.md +66 -0
  18. data/docs/checks/deprecate_lazysizes.md +61 -0
  19. data/docs/checks/liquid_tag.md +2 -2
  20. data/docs/checks/missing_template.md +25 -0
  21. data/docs/checks/pagination_size.md +44 -0
  22. data/docs/checks/template_length.md +12 -2
  23. data/docs/checks/undefined_object.md +5 -0
  24. data/lib/theme_check/analyzer.rb +25 -21
  25. data/lib/theme_check/asset_file.rb +3 -15
  26. data/lib/theme_check/bug.rb +3 -1
  27. data/lib/theme_check/check.rb +26 -4
  28. data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
  29. data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
  30. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
  31. data/lib/theme_check/checks/asset_size_css.rb +11 -74
  32. data/lib/theme_check/checks/asset_size_css_stylesheet_tag.rb +24 -0
  33. data/lib/theme_check/checks/asset_size_javascript.rb +11 -37
  34. data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
  35. data/lib/theme_check/checks/deprecate_bgsizes.rb +14 -0
  36. data/lib/theme_check/checks/deprecate_lazysizes.rb +16 -0
  37. data/lib/theme_check/checks/img_lazy_loading.rb +2 -7
  38. data/lib/theme_check/checks/img_width_and_height.rb +3 -3
  39. data/lib/theme_check/checks/liquid_tag.rb +2 -2
  40. data/lib/theme_check/checks/missing_template.rb +21 -5
  41. data/lib/theme_check/checks/pagination_size.rb +65 -0
  42. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  43. data/lib/theme_check/checks/remote_asset.rb +4 -2
  44. data/lib/theme_check/checks/space_inside_braces.rb +27 -7
  45. data/lib/theme_check/checks/template_length.rb +18 -4
  46. data/lib/theme_check/checks/undefined_object.rb +1 -1
  47. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  48. data/lib/theme_check/checks.rb +11 -1
  49. data/lib/theme_check/cli.rb +52 -15
  50. data/lib/theme_check/config.rb +56 -10
  51. data/lib/theme_check/corrector.rb +4 -0
  52. data/lib/theme_check/exceptions.rb +29 -27
  53. data/lib/theme_check/file_system_storage.rb +12 -0
  54. data/lib/theme_check/html_check.rb +1 -0
  55. data/lib/theme_check/html_node.rb +37 -16
  56. data/lib/theme_check/html_visitor.rb +17 -3
  57. data/lib/theme_check/json_check.rb +2 -2
  58. data/lib/theme_check/json_file.rb +2 -29
  59. data/lib/theme_check/json_printer.rb +26 -0
  60. data/lib/theme_check/language_server/constants.rb +8 -0
  61. data/lib/theme_check/language_server/document_link_engine.rb +40 -4
  62. data/lib/theme_check/language_server/handler.rb +6 -2
  63. data/lib/theme_check/language_server/server.rb +13 -2
  64. data/lib/theme_check/liquid_check.rb +0 -12
  65. data/lib/theme_check/node.rb +6 -4
  66. data/lib/theme_check/offense.rb +56 -3
  67. data/lib/theme_check/parsing_helpers.rb +7 -4
  68. data/lib/theme_check/position.rb +98 -14
  69. data/lib/theme_check/regex_helpers.rb +20 -0
  70. data/lib/theme_check/tags.rb +62 -8
  71. data/lib/theme_check/template.rb +3 -32
  72. data/lib/theme_check/theme.rb +2 -0
  73. data/lib/theme_check/theme_file.rb +40 -0
  74. data/lib/theme_check/version.rb +1 -1
  75. data/lib/theme_check.rb +16 -0
  76. data/theme-check.gemspec +1 -1
  77. metadata +26 -7
  78. data/bin/liquid-server +0 -4
@@ -21,8 +21,10 @@ The default configuration for this check is the following:
21
21
  ```yaml
22
22
  TemplateLength:
23
23
  enabled: true
24
- max_length: 200
24
+ max_length: 500
25
25
  exclude_schema: true
26
+ exclude_stylesheet: true
27
+ exclude_javascript: true
26
28
  ```
27
29
 
28
30
  ### `max_length`
@@ -31,7 +33,15 @@ The `max_length` (Default: `200`) option determines the maximum number of lines
31
33
 
32
34
  ### `exclude_schema`
33
35
 
34
- The `exclude_schema` (Default: `true`) option determines if the schema lines from a template should be excluded from the line count.
36
+ The `exclude_schema` (Default: `true`) option determines if the lines inside `{% schema %}` blocks from a template should be excluded from the line count.
37
+
38
+ ### `exclude_stylesheet`
39
+
40
+ The `exclude_stylesheet` (Default: `true`) option determines if the lines inside `{% stylesheet %}` blocks from a template should be excluded from the line count.
41
+
42
+ ### `exclude_javascript`
43
+
44
+ The `exclude_javascript` (Default: `true`) option determines if the lines inside `{% javascript %}` blocks from a template should be excluded from the line count.
35
45
 
36
46
  ## When Not To Use It
37
47
 
@@ -33,8 +33,13 @@ The default configuration for this check is the following:
33
33
  ```yaml
34
34
  UndefinedObject:
35
35
  enabled: true
36
+ exclude_snippets: true
36
37
  ```
37
38
 
39
+ ### `exclude_snippets`
40
+
41
+ The `exclude_snippets` (Default: `true`) option determines whether to check for undefined objects in snippets file (as objects _may_ be defined as arguments)
42
+
38
43
  ## When Not To Use It
39
44
 
40
45
  It is discouraged to disable this rule.
@@ -34,9 +34,11 @@ module ThemeCheck
34
34
 
35
35
  liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
36
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)
37
+ ThemeCheck.with_liquid_c_disabled do
38
+ @theme.liquid.each do |template|
39
+ liquid_visitor.visit_template(template)
40
+ html_visitor.visit_template(template)
41
+ end
40
42
  end
41
43
 
42
44
  @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
@@ -47,24 +49,26 @@ module ThemeCheck
47
49
  def analyze_files(files)
48
50
  reset
49
51
 
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)
52
+ ThemeCheck.with_liquid_c_disabled do
53
+ # Call all checks that run on the whole theme
54
+ liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
55
+ html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
56
+ @theme.liquid.each do |template|
57
+ liquid_visitor.visit_template(template)
58
+ html_visitor.visit_template(template)
59
+ end
60
+ @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
61
+
62
+ # Call checks that run on a single files, only on specified file
63
+ liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
64
+ html_visitor = HtmlVisitor.new(@html_checks.single_file)
65
+ files.each do |file|
66
+ if file.liquid?
67
+ liquid_visitor.visit_template(file)
68
+ html_visitor.visit_template(file)
69
+ elsif file.json?
70
+ @json_checks.single_file.call(:on_file, file)
71
+ end
68
72
  end
69
73
  end
70
74
 
@@ -1,27 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require "pathname"
3
2
  require "zlib"
4
3
 
5
4
  module ThemeCheck
6
- class AssetFile
5
+ class AssetFile < ThemeFile
7
6
  def initialize(relative_path, storage)
8
- @relative_path = relative_path
9
- @storage = storage
7
+ super
10
8
  @loaded = false
11
9
  @content = nil
12
10
  end
13
11
 
14
- def path
15
- @storage.path(@relative_path)
16
- end
17
-
18
- def relative_path
19
- @relative_pathname ||= Pathname.new(@relative_path)
20
- end
21
-
22
- def content
23
- @content ||= @storage.read(@relative_path)
24
- end
12
+ alias_method :content, :source
25
13
 
26
14
  def gzipped_size
27
15
  @gzipped_size ||= Zlib.gzip(content).bytesize
@@ -2,6 +2,8 @@
2
2
  require 'theme_check/version'
3
3
 
4
4
  module ThemeCheck
5
+ class ThemeCheckError < StandardError; end
6
+
5
7
  BUG_POSTAMBLE = <<~EOS
6
8
  Theme Check Version: #{VERSION}
7
9
  Ruby Version: #{RUBY_VERSION}
@@ -15,6 +17,6 @@ module ThemeCheck
15
17
  EOS
16
18
 
17
19
  def self.bug(message)
18
- abort(message + BUG_POSTAMBLE)
20
+ raise ThemeCheckError, message + BUG_POSTAMBLE
19
21
  end
20
22
  end
@@ -9,12 +9,19 @@ module ThemeCheck
9
9
  attr_accessor :options, :ignored_patterns
10
10
  attr_writer :offenses
11
11
 
12
+ # The order matters.
12
13
  SEVERITIES = [
13
14
  :error,
14
15
  :suggestion,
15
16
  :style,
16
17
  ]
17
18
 
19
+ # [severity: sym] => number
20
+ SEVERITY_VALUES = SEVERITIES
21
+ .map
22
+ .with_index { |sev, i| [sev, i] }
23
+ .to_h
24
+
18
25
  CATEGORIES = [
19
26
  :liquid,
20
27
  :translation,
@@ -38,6 +45,10 @@ module ThemeCheck
38
45
  @severity if defined?(@severity)
39
46
  end
40
47
 
48
+ def severity_value(severity)
49
+ SEVERITY_VALUES[severity]
50
+ end
51
+
41
52
  def categories(*categories)
42
53
  @categories ||= []
43
54
  if categories.any?
@@ -58,7 +69,7 @@ module ThemeCheck
58
69
  end
59
70
 
60
71
  def docs_url(path)
61
- "https://github.com/Shopify/theme-check/blob/master/docs/checks/#{File.basename(path, '.rb')}.md"
72
+ "https://github.com/Shopify/theme-check/blob/main/docs/checks/#{File.basename(path, '.rb')}.md"
62
73
  end
63
74
 
64
75
  def can_disable(disableable = nil)
@@ -80,12 +91,23 @@ module ThemeCheck
80
91
  @offenses ||= []
81
92
  end
82
93
 
83
- def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
84
- offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
94
+ def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, node_markup_offset: 0, &block)
95
+ offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, node_markup_offset: node_markup_offset, correction: block)
85
96
  end
86
97
 
87
98
  def severity
88
- self.class.severity
99
+ @severity ||= self.class.severity
100
+ end
101
+
102
+ def severity=(severity)
103
+ unless SEVERITIES.include?(severity)
104
+ raise ArgumentError, "unknown severity. Use: #{SEVERITIES.join(', ')}"
105
+ end
106
+ @severity = severity
107
+ end
108
+
109
+ def severity_value
110
+ SEVERITY_VALUES[severity]
89
111
  end
90
112
 
91
113
  def categories
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when invalid tags are used in a Theme App
4
+ # Extension block
5
+ class AppBlockValidTags < LiquidCheck
6
+ severity :error
7
+ category :liquid
8
+ doc docs_url(__FILE__)
9
+
10
+ # Don't allow this check to be disabled with a comment,
11
+ # since we need to be able to enforce this server-side
12
+ can_disable false
13
+
14
+ OFFENSE_MSG = "Theme app extension blocks cannot contain %s tags"
15
+
16
+ def on_javascript(node)
17
+ add_offense(OFFENSE_MSG % 'javascript', node: node)
18
+ end
19
+
20
+ def on_stylesheet(node)
21
+ add_offense(OFFENSE_MSG % 'stylesheet', node: node)
22
+ end
23
+
24
+ def on_include(node)
25
+ add_offense(OFFENSE_MSG % 'include', node: node)
26
+ end
27
+
28
+ def on_layout(node)
29
+ add_offense(OFFENSE_MSG % 'layout', node: node)
30
+ end
31
+
32
+ def on_section(node)
33
+ add_offense(OFFENSE_MSG % 'section', node: node)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when too much CSS is being referenced from a Theme App
4
+ # Extension block
5
+ class AssetSizeAppBlockCSS < LiquidCheck
6
+ severity :error
7
+ category :performance
8
+ doc docs_url(__FILE__)
9
+
10
+ # Don't allow this check to be disabled with a comment,
11
+ # since we need to be able to enforce this server-side
12
+ can_disable false
13
+
14
+ attr_reader :threshold_in_bytes
15
+
16
+ def initialize(threshold_in_bytes: 100_000)
17
+ @threshold_in_bytes = threshold_in_bytes
18
+ end
19
+
20
+ def on_schema(node)
21
+ schema = JSON.parse(node.value.nodelist.join)
22
+
23
+ if (stylesheet = schema["stylesheet"])
24
+ size = asset_size(stylesheet)
25
+ if size && size > threshold_in_bytes
26
+ add_offense(
27
+ "CSS in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
28
+ node: node
29
+ )
30
+ end
31
+ end
32
+ rescue JSON::ParserError
33
+ # Ignored, handled in ValidSchema.
34
+ end
35
+
36
+ private
37
+
38
+ def asset_size(name)
39
+ asset = @theme["assets/#{name}"]
40
+ return if asset.nil?
41
+ asset.gzipped_size
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when too much JS is being referenced from a Theme App
4
+ # Extension block
5
+ class AssetSizeAppBlockJavaScript < LiquidCheck
6
+ severity :error
7
+ category :performance
8
+ doc docs_url(__FILE__)
9
+
10
+ # Don't allow this check to be disabled with a comment,
11
+ # since we need to be able to enforce this server-side
12
+ can_disable false
13
+
14
+ attr_reader :threshold_in_bytes
15
+
16
+ def initialize(threshold_in_bytes: 10_000)
17
+ @threshold_in_bytes = threshold_in_bytes
18
+ end
19
+
20
+ def on_schema(node)
21
+ schema = JSON.parse(node.value.nodelist.join)
22
+
23
+ if (javascript = schema["javascript"])
24
+ size = asset_size(javascript)
25
+ if size && size > threshold_in_bytes
26
+ add_offense(
27
+ "JavaScript in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
28
+ node: node
29
+ )
30
+ end
31
+ end
32
+ rescue JSON::ParserError
33
+ # Ignored, handled in ValidSchema.
34
+ end
35
+
36
+ private
37
+
38
+ def asset_size(name)
39
+ asset = @theme["assets/#{name}"]
40
+ return if asset.nil?
41
+ asset.gzipped_size
42
+ end
43
+ end
44
+ end
@@ -1,89 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- class AssetSizeCSS < LiquidCheck
3
+ class AssetSizeCSS < HtmlCheck
4
4
  include RegexHelpers
5
5
  severity :error
6
- category :performance
6
+ category :html, :performance
7
7
  doc docs_url(__FILE__)
8
8
 
9
- Link = Struct.new(:href, :index)
10
-
11
- LINK_TAG_HREF = %r{
12
- <link
13
- (?=[^>]+?rel=['"]?stylesheet['"]?) # Make sure rel=stylesheet is in the link with lookahead
14
- [^>]+ # any non closing tag character
15
- href= # href attribute start
16
- (?<href>#{QUOTED_LIQUID_ATTRIBUTE}) # href attribute value (may contain liquid)
17
- [^>]* # any non closing character till the end
18
- >
19
- }omix
20
- STYLESHEET_TAG = %r{
21
- #{Liquid::VariableStart} # VariableStart
22
- (?:(?!#{Liquid::VariableEnd}).)*? # anything that isn't followed by a VariableEnd
23
- \|\s*asset_url\s* # | asset_url
24
- \|\s*stylesheet_tag\s* # | stylesheet_tag
25
- #{Liquid::VariableEnd} # VariableEnd
26
- }omix
27
-
28
9
  attr_reader :threshold_in_bytes
29
10
 
30
11
  def initialize(threshold_in_bytes: 100_000)
31
12
  @threshold_in_bytes = threshold_in_bytes
32
13
  end
33
14
 
34
- def on_document(node)
35
- @node = node
36
- @source = node.template.source
37
- record_offenses
38
- end
39
-
40
- def record_offenses
41
- stylesheets(@source).each do |stylesheet|
42
- file_size = href_to_file_size(stylesheet.href)
43
- next if file_size.nil?
44
- next if file_size <= threshold_in_bytes
45
- add_offense(
46
- "CSS on every page load exceding compressed size threshold (#{threshold_in_bytes} Bytes).",
47
- node: @node,
48
- markup: stylesheet.href,
49
- line_number: @source[0...stylesheet.index].count("\n") + 1
50
- )
51
- end
52
- end
53
-
54
- def stylesheets(source)
55
- stylesheet_links = matches(source, LINK_TAG_HREF)
56
- .map do |m|
57
- Link.new(
58
- m[:href].gsub(START_OR_END_QUOTE, ""),
59
- m.begin(:href),
60
- )
61
- end
62
-
63
- stylesheet_tags = matches(source, STYLESHEET_TAG)
64
- .map do |m|
65
- Link.new(
66
- m[0],
67
- m.begin(0),
68
- )
69
- end
70
-
71
- stylesheet_links + stylesheet_tags
72
- end
73
-
74
- def href_to_file_size(href)
75
- # asset_url (+ optional stylesheet_tag) variables
76
- if href =~ /^#{VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
77
- asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
78
- asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
79
- return if asset.nil?
80
- asset.gzipped_size
81
-
82
- # remote URLs
83
- elsif href =~ %r{^(https?:)?//}
84
- asset = RemoteAssetFile.from_src(href)
85
- asset.gzipped_size
86
- end
15
+ def on_link(node)
16
+ return if node.attributes['rel'] != "stylesheet"
17
+ file_size = href_to_file_size(node.attributes['href'])
18
+ return if file_size.nil?
19
+ return if file_size <= threshold_in_bytes
20
+ add_offense(
21
+ "CSS on every page load exceeding compressed size threshold (#{threshold_in_bytes} Bytes)",
22
+ node: node
23
+ )
87
24
  end
88
25
  end
89
26
  end