theme-check 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +5 -9
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +50 -0
  5. data/CONTRIBUTING.md +1 -1
  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 +15 -1
  10. data/config/theme_app_extension.yml +15 -0
  11. data/data/shopify_liquid/objects.yml +1 -0
  12. data/docs/checks/app_block_valid_tags.md +40 -0
  13. data/docs/checks/asset_size_app_block_css.md +1 -1
  14. data/docs/checks/deprecate_lazysizes.md +0 -3
  15. data/docs/checks/deprecated_global_app_block_type.md +65 -0
  16. data/docs/checks/missing_template.md +25 -0
  17. data/docs/checks/pagination_size.md +44 -0
  18. data/docs/checks/template_length.md +1 -1
  19. data/docs/checks/undefined_object.md +5 -0
  20. data/lib/theme_check/analyzer.rb +1 -0
  21. data/lib/theme_check/check.rb +3 -3
  22. data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
  23. data/lib/theme_check/checks/asset_size_css.rb +3 -3
  24. data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
  25. data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
  26. data/lib/theme_check/checks/default_locale.rb +3 -1
  27. data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
  28. data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
  29. data/lib/theme_check/checks/deprecated_global_app_block_type.rb +57 -0
  30. data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
  31. data/lib/theme_check/checks/img_width_and_height.rb +3 -3
  32. data/lib/theme_check/checks/missing_template.rb +21 -5
  33. data/lib/theme_check/checks/pagination_size.rb +64 -0
  34. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  35. data/lib/theme_check/checks/remote_asset.rb +3 -3
  36. data/lib/theme_check/checks/space_inside_braces.rb +27 -7
  37. data/lib/theme_check/checks/template_length.rb +1 -1
  38. data/lib/theme_check/checks/undefined_object.rb +1 -1
  39. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  40. data/lib/theme_check/checks.rb +11 -1
  41. data/lib/theme_check/cli.rb +18 -2
  42. data/lib/theme_check/corrector.rb +9 -0
  43. data/lib/theme_check/file_system_storage.rb +12 -0
  44. data/lib/theme_check/html_check.rb +0 -1
  45. data/lib/theme_check/html_node.rb +37 -16
  46. data/lib/theme_check/html_visitor.rb +17 -3
  47. data/lib/theme_check/json_check.rb +2 -2
  48. data/lib/theme_check/json_file.rb +11 -0
  49. data/lib/theme_check/json_printer.rb +27 -0
  50. data/lib/theme_check/language_server/constants.rb +18 -11
  51. data/lib/theme_check/language_server/document_link_engine.rb +3 -67
  52. data/lib/theme_check/language_server/document_link_provider.rb +71 -0
  53. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  54. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  55. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  56. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  57. data/lib/theme_check/language_server/handler.rb +17 -9
  58. data/lib/theme_check/language_server/server.rb +9 -0
  59. data/lib/theme_check/language_server/uri_helper.rb +37 -0
  60. data/lib/theme_check/language_server.rb +6 -0
  61. data/lib/theme_check/node.rb +6 -4
  62. data/lib/theme_check/offense.rb +56 -3
  63. data/lib/theme_check/parsing_helpers.rb +4 -3
  64. data/lib/theme_check/position.rb +98 -14
  65. data/lib/theme_check/regex_helpers.rb +5 -2
  66. data/lib/theme_check/theme.rb +3 -0
  67. data/lib/theme_check/version.rb +1 -1
  68. data/lib/theme_check.rb +1 -0
  69. data/theme-check.gemspec +1 -1
  70. metadata +20 -6
  71. data/bin/liquid-server +0 -4
@@ -25,8 +25,33 @@ The default configuration for this check is the following:
25
25
  ```yaml
26
26
  MissingTemplate:
27
27
  enabled: true
28
+ ignore_missing: []
28
29
  ```
29
30
 
31
+ ### `ignore_missing`
32
+
33
+ Specify a list of patterns of missing template files to ignore.
34
+
35
+ While the `ignore` option will ignore all occurrences of `MissingTemplate` according to the file in which they appear, `ignore_missing` allows ignoring all occurrences of `MissingTemplate` based on the target template, the template being rendered.
36
+
37
+ For example:
38
+
39
+ ```yaml
40
+ MissingTemplate:
41
+ ignore_missing:
42
+ - snippets/icon-*
43
+ ```
44
+
45
+ Would ignore offenses on `{% render 'icon-missing' %}` across all theme files.
46
+
47
+ ```yaml
48
+ MissingTemplate:
49
+ ignore:
50
+ - templates/index.liquid
51
+ ```
52
+
53
+ Would ignore all `MissingTemplate` in `templates/index.liquid`, no mater the file being rendered.
54
+
30
55
  ## Version
31
56
 
32
57
  This check has been introduced in Theme Check 0.1.0.
@@ -0,0 +1,44 @@
1
+ # Ensure `paginate` tags are used with performant sizes
2
+
3
+ ## Check Details
4
+
5
+ This check is aimed at keeping response times low.
6
+
7
+ :-1: Examples of **incorrect** code for this check:
8
+
9
+ ```liquid
10
+ <!-- Using too large of page size -->
11
+ {% paginate collection.products by 999 %}
12
+ ```
13
+
14
+ :+1: Examples of **correct** code for this check:
15
+
16
+ ```liquid
17
+ {% paginate collection.products by 12 %}
18
+ ```
19
+
20
+ Use sizes that are integers below the `max_size`, and above the `min_size`.
21
+
22
+ ## Check Options
23
+
24
+ The default configuration for this check is the following:
25
+
26
+ ```yaml
27
+ PaginationSize:
28
+ enabled: true
29
+ ignore: []
30
+ min_size: 1
31
+ max_size: 50
32
+ ```
33
+
34
+ ## When Not To Use It
35
+
36
+ N/A
37
+
38
+ ## Version
39
+
40
+ This check has been introduced in Theme Check 1.3.0.
41
+
42
+ ## Resources
43
+
44
+ [paginate]: https://shopify.dev/api/liquid/objects/paginate
@@ -21,7 +21,7 @@ The default configuration for this check is the following:
21
21
  ```yaml
22
22
  TemplateLength:
23
23
  enabled: true
24
- max_length: 500
24
+ max_length: 600
25
25
  exclude_schema: true
26
26
  exclude_stylesheet: true
27
27
  exclude_javascript: true
@@ -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.
@@ -87,6 +87,7 @@ module ThemeCheck
87
87
  if @auto_correct
88
88
  offenses.each(&:correct)
89
89
  @theme.liquid.each(&:write)
90
+ @theme.json.each(&:write)
90
91
  end
91
92
  end
92
93
 
@@ -69,7 +69,7 @@ module ThemeCheck
69
69
  end
70
70
 
71
71
  def docs_url(path)
72
- "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"
73
73
  end
74
74
 
75
75
  def can_disable(disableable = nil)
@@ -91,8 +91,8 @@ module ThemeCheck
91
91
  @offenses ||= []
92
92
  end
93
93
 
94
- def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
95
- 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)
96
96
  end
97
97
 
98
98
  def severity
@@ -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
@@ -13,12 +13,12 @@ module ThemeCheck
13
13
  end
14
14
 
15
15
  def on_link(node)
16
- return if node.attributes['rel']&.value != "stylesheet"
17
- file_size = href_to_file_size(node.attributes['href']&.value)
16
+ return if node.attributes['rel'] != "stylesheet"
17
+ file_size = href_to_file_size(node.attributes['href'])
18
18
  return if file_size.nil?
19
19
  return if file_size <= threshold_in_bytes
20
20
  add_offense(
21
- "CSS on every page load exceeding compressed size threshold (#{threshold_in_bytes} Bytes).",
21
+ "CSS on every page load exceeding compressed size threshold (#{threshold_in_bytes} Bytes)",
22
22
  node: node
23
23
  )
24
24
  end
@@ -16,7 +16,7 @@ module ThemeCheck
16
16
  end
17
17
 
18
18
  def on_script(node)
19
- file_size = src_to_file_size(node.attributes['src']&.value)
19
+ file_size = src_to_file_size(node.attributes['src'])
20
20
  return if file_size.nil?
21
21
  return if file_size <= threshold_in_bytes
22
22
  add_offense(
@@ -28,7 +28,7 @@ module ThemeCheck
28
28
  def src_to_file_size(src)
29
29
  # We're kind of intentionally only looking at {{ 'asset' | asset_url }} or full urls in here.
30
30
  # More complicated liquid statements are not in scope.
31
- if src =~ /^#{VARIABLE}$/o && src =~ /asset_url/ && src =~ Liquid::QuotedString
31
+ if src =~ /^#{LIQUID_VARIABLE}$/o && src =~ /asset_url/ && src =~ Liquid::QuotedString
32
32
  asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
33
33
  asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
34
34
  return if asset.nil?
@@ -7,7 +7,9 @@ module ThemeCheck
7
7
  doc docs_url(__FILE__)
8
8
 
9
9
  def on_include(node)
10
- add_offense("`include` is deprecated - convert it to `render`", node: node)
10
+ add_offense("`include` is deprecated - convert it to `render`", node: node) do |corrector|
11
+ corrector.replace(node, "render \'#{node.value.template_name_expr}\' ")
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -7,7 +7,9 @@ module ThemeCheck
7
7
 
8
8
  def on_end
9
9
  return if @theme.default_locale_json
10
- add_offense("Default translation file not found (for example locales/en.default.json)")
10
+ add_offense("Default translation file not found (for example locales/en.default.json)") do |corrector|
11
+ corrector.create_default_locale_json(@theme)
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -6,7 +6,7 @@ module ThemeCheck
6
6
  doc docs_url(__FILE__)
7
7
 
8
8
  def on_div(node)
9
- class_list = node.attributes["class"]&.value&.split(" ")
9
+ class_list = node.attributes["class"]&.split(" ")
10
10
  add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node) if class_list&.include?("lazyload")
11
11
  add_offense("Use the CSS imageset attribute instead of data-bgset", node: node) if node.attributes["data-bgset"]
12
12
  end
@@ -6,11 +6,14 @@ module ThemeCheck
6
6
  doc docs_url(__FILE__)
7
7
 
8
8
  def on_img(node)
9
- class_list = node.attributes["class"]&.value&.split(" ")
9
+ class_list = node.attributes["class"]&.split(" ")
10
+ has_loading_lazy = node.attributes["loading"] == "lazy"
11
+ has_native_source = node.attributes["src"] || node.attributes["srcset"]
12
+ return if has_native_source && has_loading_lazy
13
+ has_lazysize_source = node.attributes["data-srcset"] || node.attributes["data-src"]
14
+ has_lazysize_class = class_list&.include?("lazyload")
15
+ return unless has_lazysize_class && has_lazysize_source
10
16
  add_offense("Use the native loading=\"lazy\" attribute instead of lazysizes", node: node) if class_list&.include?("lazyload")
11
- add_offense("Use the native srcset attribute instead of data-srcset", node: node) if node.attributes["data-srcset"]
12
- add_offense("Use the native sizes attribute instead of data-sizes", node: node) if node.attributes["data-sizes"]
13
- add_offense("Do not set the data-sizes attribute to auto", node: node) if node.attributes["data-sizes"]&.value == "auto"
14
17
  end
15
18
  end
16
19
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class DeprecatedGlobalAppBlockType < LiquidCheck
4
+ severity :error
5
+ category :liquid
6
+ doc docs_url(__FILE__)
7
+
8
+ INVALID_GLOBAL_APP_BLOCK_TYPE = "@global"
9
+ VALID_GLOBAL_APP_BLOCK_TYPE = "@app"
10
+
11
+ def on_schema(node)
12
+ schema = JSON.parse(node.value.nodelist.join)
13
+
14
+ if block_types_from(schema).include?(INVALID_GLOBAL_APP_BLOCK_TYPE)
15
+ add_offense(
16
+ "Deprecated '#{INVALID_GLOBAL_APP_BLOCK_TYPE}' block type defined in the schema, use '#{VALID_GLOBAL_APP_BLOCK_TYPE}' block type instead.",
17
+ node: node
18
+ )
19
+ end
20
+ rescue JSON::ParserError
21
+ # Ignored, handled in ValidSchema.
22
+ end
23
+
24
+ def on_case(node)
25
+ if node.value == INVALID_GLOBAL_APP_BLOCK_TYPE
26
+ report_offense(node)
27
+ end
28
+ end
29
+
30
+ def on_condition(node)
31
+ if node.value.right == INVALID_GLOBAL_APP_BLOCK_TYPE || node.value.left == INVALID_GLOBAL_APP_BLOCK_TYPE
32
+ report_offense(node)
33
+ end
34
+ end
35
+
36
+ def on_variable(node)
37
+ if node.value.name == INVALID_GLOBAL_APP_BLOCK_TYPE
38
+ report_offense(node)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def report_offense(node)
45
+ add_offense(
46
+ "Deprecated '#{INVALID_GLOBAL_APP_BLOCK_TYPE}' block type, use '#{VALID_GLOBAL_APP_BLOCK_TYPE}' block type instead.",
47
+ node: node
48
+ )
49
+ end
50
+
51
+ def block_types_from(schema)
52
+ schema.fetch("blocks", []).map do |block|
53
+ block.fetch("type", "")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,7 +8,7 @@ module ThemeCheck
8
8
  ACCEPTED_LOADING_VALUES = %w[lazy eager]
9
9
 
10
10
  def on_img(node)
11
- loading = node.attributes["loading"]&.value&.downcase
11
+ loading = node.attributes["loading"]&.downcase
12
12
  return if ACCEPTED_LOADING_VALUES.include?(loading)
13
13
  if loading == "auto"
14
14
  add_offense("Prefer loading=\"lazy\" to defer loading of images", node: node)
@@ -9,8 +9,8 @@ module ThemeCheck
9
9
  ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
10
10
 
11
11
  def on_img(node)
12
- width = node.attributes["width"]&.value
13
- height = node.attributes["height"]&.value
12
+ width = node.attributes["width"]
13
+ height = node.attributes["height"]
14
14
 
15
15
  record_units_in_field_offenses("width", width, node: node)
16
16
  record_units_in_field_offenses("height", height, node: node)
@@ -35,7 +35,7 @@ module ThemeCheck
35
35
  return unless value =~ ENDS_IN_CSS_UNIT
36
36
  value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
37
37
  add_offense(
38
- "The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\".",
38
+ "The #{attribute} attribute does not take units. Replace with \"#{value_without_units}\"",
39
39
  node: node,
40
40
  )
41
41
  end
@@ -7,20 +7,36 @@ module ThemeCheck
7
7
  doc docs_url(__FILE__)
8
8
  single_file false
9
9
 
10
+ def initialize(ignore_missing: [])
11
+ @ignore_missing = ignore_missing
12
+ end
13
+
10
14
  def on_include(node)
11
15
  template = node.value.template_name_expr
12
16
  if template.is_a?(String)
13
- unless theme["snippets/#{template}"]
14
- add_offense("'snippets/#{template}.liquid' is not found", node: node)
15
- end
17
+ add_missing_offense("snippets/#{template}", node: node)
16
18
  end
17
19
  end
20
+
18
21
  alias_method :on_render, :on_include
19
22
 
20
23
  def on_section(node)
21
24
  template = node.value.section_name
22
- unless theme["sections/#{template}"]
23
- add_offense("'sections/#{template}.liquid' is not found", node: node)
25
+ add_missing_offense("sections/#{template}", node: node)
26
+ end
27
+
28
+ private
29
+
30
+ def ignore?(path)
31
+ @ignore_missing.any? { |pattern| File.fnmatch?(pattern, path) }
32
+ end
33
+
34
+ def add_missing_offense(name, node:)
35
+ path = "#{name}.liquid"
36
+ unless ignore?(path) || theme[name]
37
+ add_offense("'#{path}' is not found", node: node) do |corrector|
38
+ corrector.create(@theme, "#{name}.liquid", "")
39
+ end
24
40
  end
25
41
  end
26
42
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class PaginationSize < LiquidCheck
4
+ severity :suggestion
5
+ categories :performance
6
+ doc docs_url(__FILE__)
7
+
8
+ attr_reader :min_size
9
+ attr_reader :max_size
10
+
11
+ def initialize(min_size: 1, max_size: 50)
12
+ @min_size = min_size
13
+ @max_size = max_size
14
+ end
15
+
16
+ def on_document(_node)
17
+ @paginations = {}
18
+ @schema_settings = {}
19
+ end
20
+
21
+ def on_paginate(node)
22
+ size = node.value.page_size
23
+ unless @paginations.key?(size)
24
+ @paginations[size] = []
25
+ end
26
+ @paginations[size].push(node)
27
+ end
28
+
29
+ def on_schema(node)
30
+ schema = JSON.parse(node.value.nodelist.join)
31
+
32
+ if (settings = schema["settings"])
33
+ @schema_settings = settings
34
+ end
35
+ rescue JSON::ParserError
36
+ # Ignored, handled in ValidSchema.
37
+ end
38
+
39
+ def after_document(_node)
40
+ @paginations.each_pair do |size, nodes|
41
+ numerical_size = if size.is_a?(Numeric)
42
+ size
43
+ else
44
+ get_setting_default_value(size.lookups.last)
45
+ end
46
+ if numerical_size.nil?
47
+ nodes.each { |node| add_offense("Default pagination size should be defined in the section settings", node: node) }
48
+ elsif numerical_size > @max_size || numerical_size < @min_size || !numerical_size.is_a?(Integer)
49
+ nodes.each { |node| add_offense("Pagination size must be a positive integer between #{@min_size} and #{@max_size}", node: node) }
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def get_setting_default_value(setting_id)
57
+ setting = @schema_settings.find { |s| s['id'] == setting_id }
58
+ return nil if setting.empty?
59
+ default_value = setting['default'].to_i
60
+ return nil if default_value == 0
61
+ default_value
62
+ end
63
+ end
64
+ end