theme-check 0.2.2 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d699b72927dc2112b7cf3560a303a3dff52dfd99120e3201cd9721372282e36
4
- data.tar.gz: e850929c23554d200de3ca17e0759e8a760f98152cf293561a54aa5fbc24c5e5
3
+ metadata.gz: a07597b217b1f596cd5e777726950e396934726f3572bc7660129bec2b3b30c8
4
+ data.tar.gz: 80e54966286e332f3308021b9347feb4cdb683497991b9bb451dcf58f138a2df
5
5
  SHA512:
6
- metadata.gz: f3e26f98b06a8c596442ac7562d72810ab92b4e20a3ddca2892860f94377fb1b5f4e9d6ba94eba5687170c364ac17a080fb971b95cc41b66d3a811b2637282ab
7
- data.tar.gz: c2d95ee213323de5df634c2b47b21317a3fef5f9edae2aae056d87ecc41694a0ba9296eb9dd0c9cf70213ad920d2b8fa4849d197c77cb196c50111728156eca5
6
+ metadata.gz: 4c7a1c159dd1b688618c95e1e5fb943a133b606878f57c91b12e5793b07022325c5cfb2e81c49cb45ea484cb3688c6f7c2b75af1b2a50f13da6db2ba0e8b0bb3
7
+ data.tar.gz: 83fe6a74375ecf2b58f14681c99bc672a710cd6bbc70cb5f58ff06332d4123171544f6cf73c6e7f5f750c1a870d17de0ce3bd3e1f9cbfca6e1bdc7fceb767b80
data/CONTRIBUTING.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  We love receiving pull requests!
4
4
 
5
+ For your contribution to be accepted you will need to sign the [Shopify Contributor License Agreement (CLA)](https://cla.shopify.com/).
6
+
5
7
  ## Standards
6
8
 
7
9
  * Checks should do one thing, and do it well.
data/README.md CHANGED
@@ -4,7 +4,7 @@ Think RuboCop, or eslint, but for Shopify themes.
4
4
 
5
5
  Theme Check is a command line tool that helps you follow Shopify Themes & Liquid best practices by analyzing the Liquid & JSON inside your theme.
6
6
 
7
- Code editor support coming soon!
7
+ Theme Check is also available [inside some code editors](https://github.com/Shopify/theme-check/wiki).
8
8
 
9
9
  ![](docs/preview.png)
10
10
 
@@ -30,8 +30,9 @@ Theme Check currently checks for the following:
30
30
  ✅ Unmatching translation keys in locale files
31
31
  ✅ Using unknown translation keys in `{{ 'missing_key' | t }}`
32
32
  ✅ Using several `{% ... %}` instead of `{% liquid ... %}`
33
- ✅ Undefined [objects](https://shopify.dev/docs/themes/liquid/reference/objects)
33
+ ✅ Undefined [objects](https://shopify.dev/docs/themes/liquid/reference/objects)
34
34
  ✅ Deprecated filters
35
+ ✅ Missing `theme-check-enable` comment
35
36
 
36
37
  And many more to come! Suggestions welcome ([create an issue](https://github.com/Shopify/theme-check/issues)).
37
38
 
@@ -79,11 +80,53 @@ Add a `.theme-check.yml` file at the root of your theme to configure:
79
80
  # be uploaded to Shopify.
80
81
  root: dist
81
82
 
83
+ # It is possible to extend theme-check with custom checks
84
+ require:
85
+ - ./path/to/my_custom_check.rb
86
+
82
87
  # Disable some checks
83
88
  TemplateLength:
84
89
  enabled: false
85
90
  # Or configure options
86
91
  max_length: 300
92
+
93
+ # Enable a custom check
94
+ MyCustomCheck
95
+ enabled: true
87
96
  ```
88
97
 
89
98
  See [config/default.yml](config/default.yml) for available options & defaults.
99
+
100
+ ## Disable checks with comments
101
+
102
+ Use Liquid comments to disable and re-enable all checks for a section of your template:
103
+
104
+ ```liquid
105
+ {% comment %}theme-check-disable{% endcomment %}
106
+ {% assign x = 1 %}
107
+ {% comment %}theme-check-enable{% endcomment %}
108
+ ```
109
+
110
+ Disable a specific check by including it in the comment:
111
+
112
+ ```liquid
113
+ {% comment %}theme-check-disable UnusedAssign{% endcomment %}
114
+ {% assign x = 1 %}
115
+ {% comment %}theme-check-enable UnusedAssign{% endcomment %}
116
+ ```
117
+
118
+ Disable multiple checks by including them as a comma-separated list:
119
+
120
+ ```liquid
121
+ {% comment %}theme-check-disable UnusedAssign,SpaceInsideBraces{% endcomment %}
122
+ {%assign x = 1%}
123
+ {% comment %}theme-check-enable UnusedAssign,SpaceInsideBraces{% endcomment %}
124
+ ```
125
+
126
+ Disable checks for the _entire document_ by placing the comment on the first line:
127
+
128
+ ```liquid
129
+ {% comment %}theme-check-disable SpaceInsideBraces{% endcomment %}
130
+
131
+ {%assign x = 1%}
132
+ ```
data/RELEASING.md ADDED
@@ -0,0 +1,41 @@
1
+ ## Releasing Theme Check
2
+
3
+ 1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org
4
+
5
+ 2. Create a PR to update the version in `lib/theme_check/version.rb`
6
+
7
+ 3. Merge your PR to master
8
+
9
+ 4. On [Shipit](https://shipit.shopify.io/shopify/theme-check/rubygems), deploy your commit.
10
+
11
+ ## Homebrew Release Process
12
+
13
+ 1. Release `theme-check` on RubyGems by following the steps in the previous section.
14
+
15
+ 2. Generate the homebrew formula.
16
+
17
+ ```bash
18
+ rake package
19
+ ```
20
+
21
+ 3. Copy the formula over in the [`homebrew-shopify`](https://github.com/Shopify/homebrew-shopify) repository.
22
+
23
+ ```bash
24
+ VERSION=X.X.X
25
+ cp packaging/builds/$VERSION/theme-check ../homebrew-shopify
26
+ ```
27
+
28
+ 4. Create a branch + a commit on the [`homebrew-shopify`](https://github.com/Shopify/homebrew-shopify) repository.
29
+
30
+ ```bash
31
+ git checkout -b "bump/theme-check-$VERSION"
32
+ git add theme-check.rb
33
+ git commit -m "Bump theme-check version to $VERSION"
34
+ ```
35
+
36
+ 5. Create a pull-request for those changes on the [`homebrew-shopify`](https://github.com/Shopify/homebrew-shopify) repository.
37
+
38
+ ```bash
39
+ # shortcut if you have `hub` installed
40
+ hub compare "master:bump/theme-check-$VERSION"
41
+ ```
data/Rakefile CHANGED
@@ -3,12 +3,32 @@ require "rake/testtask"
3
3
  require "rubocop/rake_task"
4
4
  require "bundler/gem_tasks"
5
5
 
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
6
+ namespace :tests do
7
+ task all: [:in_memory, :file_system]
8
+
9
+ Rake::TestTask.new(:suite) do |t|
10
+ t.libs << "test"
11
+ t.libs << "lib"
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ end
14
+
15
+ desc("Runs the tests with InMemoryStorage")
16
+ task :in_memory do
17
+ ENV["THEME_STORAGE"] = 'InMemoryStorage'
18
+ puts "Running tests with #{ENV['THEME_STORAGE']}"
19
+ Rake::Task['tests:suite'].execute
20
+ end
21
+
22
+ desc("Runs the tests with FileSystemStorage")
23
+ task :file_system do
24
+ ENV["THEME_STORAGE"] = 'FileSystemStorage'
25
+ puts "Running tests with #{ENV['THEME_STORAGE']}"
26
+ Rake::Task['tests:suite'].execute
27
+ end
10
28
  end
11
29
 
30
+ task(test: 'tests:all')
31
+
12
32
  RuboCop::RakeTask.new
13
33
 
14
34
  task default: [:test, :rubocop]
data/config/default.yml CHANGED
@@ -1,3 +1,10 @@
1
+ root: .
2
+
3
+ require: []
4
+
5
+ ignore:
6
+ - node_modules/*
7
+
1
8
  ConvertIncludeToRender:
2
9
  enabled: true
3
10
 
@@ -22,6 +29,8 @@ SyntaxError:
22
29
  TemplateLength:
23
30
  enabled: true
24
31
  max_length: 200
32
+ # Exclude content of {% schema %} in line count
33
+ exclude_schema: true
25
34
 
26
35
  UnknownFilter:
27
36
  enabled: true
@@ -64,3 +73,9 @@ RequiredDirectories:
64
73
 
65
74
  DeprecatedFilter:
66
75
  enabled: true
76
+
77
+ MissingEnableComment:
78
+ enabled: true
79
+
80
+ ParserBlockingJavaScript:
81
+ enabled: true
@@ -0,0 +1,15 @@
1
+ ---
2
+ - alternative_payment_methods
3
+ - breadcrumb
4
+ - checkout_html_classes
5
+ - checkout_scripts
6
+ - checkout_stylesheets
7
+ - content_for_footer
8
+ - content_for_logo
9
+ - content_for_order_summary
10
+ - direction
11
+ - locale
12
+ - order_summary_toggle
13
+ - page_title
14
+ - skip_to_content_link
15
+ - tracking_code
data/dev.yml CHANGED
@@ -19,5 +19,7 @@ commands:
19
19
  run: bundle exec rake test
20
20
  style:
21
21
  run: bundle exec rake rubocop
22
+ autocorrect:
23
+ run: bundle exec rubocop --auto-correct
22
24
  language-server:
23
25
  run: bundle exec theme-check-language-server
data/lib/theme_check.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "theme_check/analyzer"
5
5
  require_relative "theme_check/check"
6
6
  require_relative "theme_check/checks_tracking"
7
7
  require_relative "theme_check/cli"
8
+ require_relative "theme_check/disabled_checks"
8
9
  require_relative "theme_check/liquid_check"
9
10
  require_relative "theme_check/locale_diff"
10
11
  require_relative "theme_check/json_check"
@@ -17,6 +18,9 @@ require_relative "theme_check/node"
17
18
  require_relative "theme_check/offense"
18
19
  require_relative "theme_check/printer"
19
20
  require_relative "theme_check/shopify_liquid"
21
+ require_relative "theme_check/storage"
22
+ require_relative "theme_check/file_system_storage"
23
+ require_relative "theme_check/in_memory_storage"
20
24
  require_relative "theme_check/tags"
21
25
  require_relative "theme_check/template"
22
26
  require_relative "theme_check/theme"
@@ -35,12 +35,6 @@ module ThemeCheck
35
35
  @offenses
36
36
  end
37
37
 
38
- def analyze_file(path)
39
- path = Pathname.new(path)
40
- analyze_theme
41
- @offenses.reject! { |offense| offense.template.path != path }
42
- end
43
-
44
38
  def uncorrectable_offenses
45
39
  unless @auto_correct
46
40
  return @offenses
@@ -50,6 +50,13 @@ module ThemeCheck
50
50
  @doc = doc if doc
51
51
  @doc if defined?(@doc)
52
52
  end
53
+
54
+ def can_disable(disableable = nil)
55
+ unless disableable.nil?
56
+ @can_disable = disableable
57
+ end
58
+ defined?(@can_disable) ? @can_disable : true
59
+ end
53
60
  end
54
61
 
55
62
  def severity
@@ -80,6 +87,10 @@ module ThemeCheck
80
87
  defined?(@ignored) && @ignored
81
88
  end
82
89
 
90
+ def can_disable?
91
+ self.class.can_disable
92
+ end
93
+
83
94
  def to_s
84
95
  s = +"#{code_name}:\n"
85
96
  properties = { severity: severity, category: category, doc: doc }.merge(options)
@@ -8,5 +8,15 @@ module ThemeCheck
8
8
  end
9
9
  end
10
10
  end
11
+
12
+ def always_enabled
13
+ self.class.new(reject(&:can_disable?))
14
+ end
15
+
16
+ def except_for(disabled_checks)
17
+ still_enabled = reject { |check| disabled_checks.all.include?(check.code_name) }
18
+
19
+ self.class.new((always_enabled + still_enabled).uniq)
20
+ end
11
21
  end
12
22
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class MissingEnableComment < LiquidCheck
4
+ severity :error
5
+
6
+ # Don't allow this check to be disabled with a comment,
7
+ # as we need to be able to check for disabled checks.
8
+ can_disable false
9
+
10
+ def on_document(_node)
11
+ @disabled_checks = DisabledChecks.new
12
+ end
13
+
14
+ def on_comment(node)
15
+ @disabled_checks.update(node)
16
+ end
17
+
18
+ def after_document(node)
19
+ return if @disabled_checks.full_document_disabled?
20
+ return unless @disabled_checks.any?
21
+
22
+ message = if @disabled_checks.all_disabled?
23
+ "All checks were"
24
+ else
25
+ @disabled_checks.all.join(', ') + " " + (@disabled_checks.all.size == 1 ? "was" : "were")
26
+ end
27
+
28
+ add_offense("#{message} disabled but not re-enabled with theme-check-enable", node: node)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when trying to use parser-blocking script tags
4
+ class ParserBlockingJavaScript < LiquidCheck
5
+ severity :error
6
+ category :liquid
7
+
8
+ PARSER_BLOCKING_SCRIPT_TAG = %r{
9
+ <script # Find the start of a script tag
10
+ (?=(?:[^>]|\n|\r)+?src=)+? # Make sure src= is in the script with a lookahead
11
+ (?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
12
+ >
13
+ }xim
14
+ SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
15
+
16
+ def on_document(node)
17
+ @source = node.template.source
18
+ @node = node
19
+ record_offenses
20
+ end
21
+
22
+ private
23
+
24
+ def record_offenses
25
+ record_offenses_from_regex(
26
+ message: "Missing async or defer attribute on script tag",
27
+ regex: PARSER_BLOCKING_SCRIPT_TAG,
28
+ )
29
+ record_offenses_from_regex(
30
+ message: "The script_tag filter is parser-blocking. Use a script tag with the async or defer attribute for better performance",
31
+ regex: SCRIPT_TAG_FILTER,
32
+ )
33
+ end
34
+
35
+ # The trickiness here is matching on scripts that are defined on
36
+ # multiple lines (or repeat matches). This makes the line_number
37
+ # calculation a bit weird. So instead, we traverse the string in
38
+ # a very imperative way.
39
+ def record_offenses_from_regex(regex: nil, message: nil)
40
+ i = 0
41
+ while (i = @source.index(regex, i))
42
+ script = @source.match(regex, i)[0]
43
+
44
+ add_offense(
45
+ message,
46
+ node: @node,
47
+ markup: script,
48
+ line_number: @source[0...i].count("\n") + 1
49
+ )
50
+
51
+ i += script.size
52
+ end
53
+ end
54
+ end
55
+ end
@@ -11,6 +11,7 @@ module ThemeCheck
11
11
 
12
12
  def on_node(node)
13
13
  return unless node.markup
14
+ return if :assign == node.type_name
14
15
 
15
16
  outside_of_strings(node.markup) do |chunk|
16
17
  chunk.scan(/([,:]) +/) do |_match|
@@ -4,12 +4,20 @@ module ThemeCheck
4
4
  severity :suggestion
5
5
  category :liquid
6
6
 
7
- def initialize(max_length: 200)
7
+ def initialize(max_length: 200, exclude_schema: true)
8
8
  @max_length = max_length
9
+ @exclude_schema = exclude_schema
10
+ @excluded_lines = 0
9
11
  end
10
12
 
11
- def on_document(node)
12
- lines = node.template.source.count("\n")
13
+ def on_schema(node)
14
+ if @exclude_schema
15
+ @excluded_lines += node.value.nodelist.join.count("\n")
16
+ end
17
+ end
18
+
19
+ def after_document(node)
20
+ lines = node.template.source.count("\n") - @excluded_lines
13
21
  if lines > @max_length
14
22
  add_offense("Template has too many lines [#{lines}/#{@max_length}]", template: node.template)
15
23
  end
@@ -95,11 +95,19 @@ module ThemeCheck
95
95
  all_global_objects = ThemeCheck::ShopifyLiquid::Object.labels
96
96
  all_global_objects.freeze
97
97
 
98
+ shopify_plus_objects = ThemeCheck::ShopifyLiquid::Object.plus_labels
99
+ shopify_plus_objects.freeze
100
+
98
101
  each_template do |(name, info)|
99
102
  if 'templates/customers/reset_password' == name
100
103
  # NOTE: `email` is exceptionally exposed as a theme object in
101
104
  # the customers' reset password template
102
105
  check_object(info, all_global_objects + ['email'])
106
+ elsif 'layout/checkout' == name
107
+ # NOTE: Shopify Plus has exceptionally exposed objects in
108
+ # the checkout template
109
+ # https://shopify.dev/docs/themes/theme-templates/checkout-liquid#optional-objects
110
+ check_object(info, all_global_objects + shopify_plus_objects)
103
111
  else
104
112
  check_object(info, all_global_objects)
105
113
  end