theme-check 1.0.0 → 1.4.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +2 -6
  3. data/CHANGELOG.md +50 -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 +28 -1
  10. data/config/nothing.yml +11 -0
  11. data/config/theme_app_extension.yml +168 -0
  12. data/data/shopify_liquid/objects.yml +1 -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/deprecate_lazysizes.md +0 -3
  17. data/docs/checks/missing_template.md +25 -0
  18. data/docs/checks/pagination_size.md +44 -0
  19. data/docs/checks/template_length.md +1 -1
  20. data/docs/checks/undefined_object.md +5 -0
  21. data/lib/theme_check/analyzer.rb +26 -21
  22. data/lib/theme_check/asset_file.rb +3 -15
  23. data/lib/theme_check/bug.rb +3 -1
  24. data/lib/theme_check/check.rb +26 -4
  25. data/lib/theme_check/checks/app_block_valid_tags.rb +36 -0
  26. data/lib/theme_check/checks/asset_size_app_block_css.rb +44 -0
  27. data/lib/theme_check/checks/asset_size_app_block_javascript.rb +44 -0
  28. data/lib/theme_check/checks/asset_size_css.rb +3 -3
  29. data/lib/theme_check/checks/asset_size_javascript.rb +2 -2
  30. data/lib/theme_check/checks/convert_include_to_render.rb +3 -1
  31. data/lib/theme_check/checks/default_locale.rb +3 -1
  32. data/lib/theme_check/checks/deprecate_bgsizes.rb +1 -1
  33. data/lib/theme_check/checks/deprecate_lazysizes.rb +7 -4
  34. data/lib/theme_check/checks/img_lazy_loading.rb +1 -1
  35. data/lib/theme_check/checks/img_width_and_height.rb +3 -3
  36. data/lib/theme_check/checks/missing_template.rb +21 -5
  37. data/lib/theme_check/checks/pagination_size.rb +65 -0
  38. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  39. data/lib/theme_check/checks/remote_asset.rb +3 -3
  40. data/lib/theme_check/checks/space_inside_braces.rb +27 -7
  41. data/lib/theme_check/checks/template_length.rb +1 -1
  42. data/lib/theme_check/checks/undefined_object.rb +1 -1
  43. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  44. data/lib/theme_check/checks.rb +11 -1
  45. data/lib/theme_check/cli.rb +52 -15
  46. data/lib/theme_check/config.rb +56 -10
  47. data/lib/theme_check/corrector.rb +9 -0
  48. data/lib/theme_check/exceptions.rb +29 -27
  49. data/lib/theme_check/file_system_storage.rb +12 -0
  50. data/lib/theme_check/html_check.rb +0 -1
  51. data/lib/theme_check/html_node.rb +37 -16
  52. data/lib/theme_check/html_visitor.rb +17 -3
  53. data/lib/theme_check/json_check.rb +2 -2
  54. data/lib/theme_check/json_file.rb +11 -27
  55. data/lib/theme_check/json_printer.rb +26 -0
  56. data/lib/theme_check/language_server/constants.rb +21 -6
  57. data/lib/theme_check/language_server/document_link_engine.rb +3 -31
  58. data/lib/theme_check/language_server/document_link_provider.rb +70 -0
  59. data/lib/theme_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  60. data/lib/theme_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  61. data/lib/theme_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  62. data/lib/theme_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  63. data/lib/theme_check/language_server/handler.rb +7 -4
  64. data/lib/theme_check/language_server/server.rb +13 -2
  65. data/lib/theme_check/language_server.rb +5 -0
  66. data/lib/theme_check/node.rb +6 -4
  67. data/lib/theme_check/offense.rb +56 -3
  68. data/lib/theme_check/parsing_helpers.rb +4 -3
  69. data/lib/theme_check/position.rb +98 -14
  70. data/lib/theme_check/regex_helpers.rb +5 -2
  71. data/lib/theme_check/tags.rb +26 -9
  72. data/lib/theme_check/template.rb +3 -32
  73. data/lib/theme_check/theme.rb +3 -0
  74. data/lib/theme_check/theme_file.rb +40 -0
  75. data/lib/theme_check/version.rb +1 -1
  76. data/lib/theme_check.rb +16 -0
  77. data/theme-check.gemspec +1 -1
  78. metadata +24 -6
  79. data/bin/liquid-server +0 -4
@@ -54,6 +54,7 @@
54
54
  - part
55
55
  - policy
56
56
  - powered_by_link
57
+ - predictive_search
57
58
  - product
58
59
  - product_option
59
60
  - product_variant
@@ -0,0 +1,40 @@
1
+ # Reject Forbidden Tags from Theme App Extension Blocks (`AppBlockValidTags`)
2
+
3
+ This rule exists to prevent theme app extension blocks from containing forbidden tags in their liquid code.
4
+
5
+ ## Check Details
6
+
7
+ This rule verifies none of the below tags are used in theme app extension blocks.
8
+
9
+ - `{% javascript %}`
10
+ - `{% stylesheet %}`
11
+ - `{% include 'foo' %}`
12
+ - `{% layout 'foo' %}`
13
+ - `{% section 'foo' %}`
14
+
15
+ :-1: **Incorrect** code for this check occurs with the use of any of the above tags in the liquid code of theme app extension blocks.
16
+
17
+ ## Check Options
18
+
19
+ The default configuration for theme app extensions is the following:
20
+
21
+ ```yaml
22
+ AppBlockValidTags:
23
+ enabled: true
24
+ ```
25
+
26
+ ## When Not To Use It
27
+
28
+ This rule should not be disabled locally.
29
+
30
+ ## Version
31
+
32
+ This check has been introduced in 1.3.0
33
+
34
+ ## Resources
35
+
36
+ - [Rule Source][codesource]
37
+ - [Documentation Source][docsource]
38
+
39
+ [codesource]: /lib/theme_check/checks/app_block_valid_tags.rb
40
+ [docsource]: /docs/checks/app_block_valid_tags.md
@@ -0,0 +1,52 @@
1
+ # Prevent Large CSS bundles (`AssetSizeAppBlockCSS`)
2
+
3
+ This rule exists to prevent large CSS bundles from being included via Theme App Extensions (for speed).
4
+
5
+ ## Check Details
6
+
7
+ This rule disallows the use of too much CSS in themes, as configured by `threshold_in_bytes`.
8
+
9
+ :-1: Examples of **incorrect** code for this check:
10
+ ```liquid
11
+ <!-- Here, assets/app.css is **greater** than `threshold_in_bytes` compressed. -->
12
+ {% schema %}
13
+ {
14
+ ...
15
+ "stylesheet": "app.css"
16
+ }
17
+ {% endschema %}
18
+ ```
19
+
20
+ ## Check Options
21
+
22
+ The default configuration is the following:
23
+
24
+ ```yaml
25
+ AssetSizeAppBlockCSS:
26
+ enabled: true
27
+ threshold_in_bytes: 100_000
28
+ ```
29
+
30
+ ### `threshold_in_bytes`
31
+
32
+ The `threshold_in_bytes` option (default: `100_000`) determines the maximum allowed compressed size in bytes that a single CSS file can take.
33
+
34
+ This includes theme and remote stylesheets.
35
+
36
+ ## When Not To Use It
37
+
38
+ This rule should not be disabled locally since the check will be enforced when
39
+ promoting new versions of the extension.
40
+
41
+ ## Version
42
+
43
+ This check has been introduced in 1.1.0
44
+
45
+ ## Resources
46
+
47
+ - [The Performance Inequality Gap](https://infrequently.org/2021/03/the-performance-inequality-gap/)
48
+ - [Rule Source][codesource]
49
+ - [Documentation Source][docsource]
50
+
51
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_css.rb
52
+ [docsource]: /docs/checks/asset_size_app_block_css.md
@@ -0,0 +1,57 @@
1
+ # Prevent Abuse on Server Rendered App Blocks (`AssetSizeAppBlockJavaScript`)
2
+
3
+ For server rendered app blocks, it is an anti-pattern to execute large JavaScript bundles on every page load
4
+
5
+ This doesn't mean they don't have a reason to exist. For instance, chat widgets are mini applications embedded inside web pages. Designing such an app with server rendered updates would be absurd. However, if only 10% of the users interact with the chat widget, the other 90% should not have to execute the entire bundle on every page load.
6
+
7
+ The natural solution to this problem is to implement the chat widget using the [Import on Interaction Pattern][ioip].
8
+
9
+ ## Check Details
10
+
11
+ This rule disallows the use of block JavaScript files and external scripts to have a compressed size greater than a configured `threshold_in_bytes`.
12
+
13
+ :-1: Examples of **incorrect** code for this check:
14
+ ```liquid
15
+ <!-- Here assets/chat-widget.js is more than 10KB gzipped. -->
16
+ {% schema %}
17
+ {
18
+ ...
19
+ "javascript": "chat-widget.js"
20
+ }
21
+ {% endschema %}
22
+ ```
23
+
24
+ ## Check Options
25
+
26
+ The default configuration is the following:
27
+
28
+ ```yaml
29
+ AssetSizeAppBlockJavaScript:
30
+ enabled: true
31
+ threshold_in_bytes: 10000
32
+ ```
33
+
34
+ ### `threshold_in_bytes`
35
+
36
+ The `threshold_in_bytes` option (default: `10000`) determines the maximum allowed compressed size in bytes that a single JavaScript file can take.
37
+
38
+ This includes theme and remote scripts.
39
+
40
+ ## When Not To Use It
41
+
42
+ This rule should not be disabled locally since the check will be enforced when
43
+ promoting new versions of the extension.
44
+
45
+ ## Version
46
+
47
+ This check has been introduced in 1.1.0
48
+
49
+ ## Resources
50
+
51
+ - [The Import On Interaction Pattern][ioip]
52
+ - [Rule Source][codesource]
53
+ - [Documentation Source][docsource]
54
+
55
+ [ioip]: https://addyosmani.com/blog/import-on-interaction/
56
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_javascript.rb
57
+ [docsource]: /docs/checks/asset_size_app_block_javascript.md
@@ -10,9 +10,6 @@ This check is aimed at discouraging the use of the lazysizes JavaScript library
10
10
 
11
11
  ```liquid
12
12
 
13
- <!-- Reports use of "lazyload" class -->
14
- <img src="a.jpg" class="lazyload">
15
-
16
13
  <!-- Reports use of "data-srcset" and "data-sizes" attribute. Reports data-sizes="auto" -->
17
14
  <img
18
15
  alt="House by the lake"
@@ -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.
@@ -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
 
@@ -83,6 +87,7 @@ module ThemeCheck
83
87
  if @auto_correct
84
88
  offenses.each(&:correct)
85
89
  @theme.liquid.each(&:write)
90
+ @theme.json.each(&:write)
86
91
  end
87
92
  end
88
93
 
@@ -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