theme-check 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +10 -3
  3. data/CHANGELOG.md +8 -0
  4. data/Gemfile +5 -5
  5. data/LICENSE.md +2 -0
  6. data/README.md +1 -0
  7. data/RELEASING.md +10 -3
  8. data/Rakefile +6 -0
  9. data/config/default.yml +3 -0
  10. data/docs/checks/remote_asset.md +82 -0
  11. data/lib/theme_check.rb +1 -0
  12. data/lib/theme_check/check.rb +1 -1
  13. data/lib/theme_check/checks/asset_size_css.rb +1 -1
  14. data/lib/theme_check/checks/asset_size_javascript.rb +1 -1
  15. data/lib/theme_check/checks/img_width_and_height.rb +2 -2
  16. data/lib/theme_check/checks/matching_translations.rb +1 -1
  17. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  18. data/lib/theme_check/checks/remote_asset.rb +98 -0
  19. data/lib/theme_check/checks/translation_key_exists.rb +1 -4
  20. data/lib/theme_check/checks/undefined_object.rb +1 -1
  21. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  22. data/lib/theme_check/cli.rb +13 -4
  23. data/lib/theme_check/disabled_checks.rb +2 -2
  24. data/lib/theme_check/in_memory_storage.rb +13 -16
  25. data/lib/theme_check/language_server/completion_engine.rb +2 -2
  26. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -1
  27. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
  28. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +1 -1
  29. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +2 -2
  30. data/lib/theme_check/language_server/document_link_engine.rb +4 -3
  31. data/lib/theme_check/language_server/handler.rb +29 -21
  32. data/lib/theme_check/language_server/server.rb +1 -2
  33. data/lib/theme_check/node.rb +1 -2
  34. data/lib/theme_check/offense.rb +1 -1
  35. data/lib/theme_check/releaser.rb +39 -0
  36. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +6 -8
  37. data/lib/theme_check/shopify_liquid/filter.rb +3 -5
  38. data/lib/theme_check/shopify_liquid/object.rb +2 -6
  39. data/lib/theme_check/shopify_liquid/tag.rb +1 -3
  40. data/lib/theme_check/storage.rb +3 -3
  41. data/lib/theme_check/string_helpers.rb +47 -0
  42. data/lib/theme_check/tags.rb +1 -2
  43. data/lib/theme_check/theme.rb +1 -1
  44. data/lib/theme_check/version.rb +1 -1
  45. data/theme-check.gemspec +1 -2
  46. metadata +8 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93631c68de37297060f57799731a0ef4adae05aa9d89f8a019d28b0afe65533a
4
- data.tar.gz: 86efb9c580a6206974bec0c876fc1feca80c5dad3c7d500e4528a537f01c9613
3
+ metadata.gz: c75935127b3ec2257d0e857c46b501a0c5189da619776202a2180234606c84ca
4
+ data.tar.gz: 3a54ffed715331b78f6ae3949b76466068539cd9fea4275e64f9f7a413ffd22b
5
5
  SHA512:
6
- metadata.gz: 712d137ecb52bc6af0b83db9ed391252f78d5a0ad2a3b4c3d82d6b1a59e927da167b2bdea40a7f07f45d77cb16c72ca2e49243b78bb6c52296ed84a30c5cd83f
7
- data.tar.gz: 3e6505817fc868979c2ead8f99133d9c24754b905b3211774c74e9a1261a9f962fd9660c94aed70838a2c0dcb086e353a7ce5a739d9ada8456f7ba0ff61ee077
6
+ metadata.gz: 2720180bf217fd64061f6ed4edc5ba01616fbb6ec6b30254638c992924719d179af0f2d72c4a1a1330c7b1e6a0ef9856aec4567aca73161a0cd2b59033fffcf4
7
+ data.tar.gz: c1d79dad05a55c374d4a2e008484d3c527f4483174e66ffb3c30ce96f65e8a2f03c6295de33130295b70bc63f3d3aed4dce2c7eac14de0347aa73fca599b062e
@@ -8,15 +8,22 @@ on:
8
8
 
9
9
  jobs:
10
10
  test:
11
+ runs-on: macos-latest
11
12
 
12
- runs-on: ubuntu-20.04
13
+ strategy:
14
+ matrix:
15
+ version:
16
+ - 3.0.0
17
+ - 2.7.1
18
+
19
+ name: Ruby ${{ matrix.version }}
13
20
 
14
21
  steps:
15
22
  - uses: actions/checkout@v2
16
- - name: Set up Ruby
23
+ - name: Set up Ruby ${{ matrix.version }}
17
24
  uses: ruby/setup-ruby@v1
18
25
  with:
19
- ruby-version: 2.7
26
+ ruby-version: ${{ matrix.version }}
20
27
  - uses: actions/cache@v1
21
28
  with:
22
29
  path: vendor/bundle
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
1
 
2
+ 0.7.0 / 2021-04-08
3
+ ==================
4
+
5
+ * Add [RemoteAsset Check](/docs/checks/remote_asset.md)
6
+ * Fixes:
7
+ * Don't hang on self closing img tags ([#247](https://github.com/shopify/theme-check/issues/247))
8
+ * Fix document links from different root
9
+
2
10
  0.6.0 / 2021-03-23
3
11
  ==================
4
12
 
data/Gemfile CHANGED
@@ -21,8 +21,8 @@ group :development do
21
21
  gem 'guard-minitest'
22
22
  end
23
23
 
24
- gem 'rubocop', require: false
25
- gem 'rubocop-performance', require: false
26
- gem 'rubocop-shopify', require: false
27
- gem 'rubocop-minitest', require: false
28
- gem 'rubocop-rake', require: false
24
+ gem 'rubocop', '~> 1.12.0', require: false
25
+ gem 'rubocop-performance', '~> 1.10.2', require: false
26
+ gem 'rubocop-shopify', '~> 1.0.7', require: false
27
+ gem 'rubocop-minitest', '~> 0.11.0', require: false
28
+ gem 'rubocop-rake', '~> 0.5.1', require: false
data/LICENSE.md CHANGED
@@ -1,6 +1,8 @@
1
1
 
2
2
  Copyright 2020-present, Shopify Inc.
3
3
 
4
+ Contains code from activesupport Copyright (c) 2005-2019 David Heinemeier Hansson
5
+
4
6
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
7
 
6
8
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
data/README.md CHANGED
@@ -35,6 +35,7 @@ Theme Check currently checks for the following:
35
35
  As well as checks that prevent easy to spot performance problems:
36
36
 
37
37
  ✅ Use of [parser-blocking](/docs/checks/parser_blocking_javascript.md) JavaScript
38
+ ✅ [Use of non-Shopify domains for assets](/docs/checks/remote_asset.md)
38
39
  ✅ [Missing width and height attributes on `img` tags](/docs/checks/img_width_and_height.md)
39
40
  ✅ [Too much JavaScript](/docs/checks/asset_size_javascript.md)
40
41
  ✅ [Too much CSS](/docs/checks/asset_size_css.md)
data/RELEASING.md CHANGED
@@ -2,11 +2,18 @@
2
2
 
3
3
  1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org
4
4
 
5
- 2. Create a PR to update the version in `lib/theme_check/version.rb` and replace the `THEME_CHECK_VERSION` placeholder in the documentation for new rules.
5
+ 2. Run the following command to update the version in `lib/theme_check/version.rb` and replace the `THEME_CHECK_VERSION` placeholder in the documentation for new rules:
6
6
 
7
- 3. Merge your PR to master
7
+ ```bash
8
+ VERSION="X.X.X"
9
+ rake prerelease[$VERSION]
10
+ ```
11
+
12
+ 3. Commit your changes and make a PR.
13
+
14
+ 4. Merge your PR to master.
8
15
 
9
- 4. On [Shipit](https://shipit.shopify.io/shopify/theme-check/rubygems), deploy your commit.
16
+ 5. On [Shipit](https://shipit.shopify.io/shopify/theme-check/rubygems), deploy your commit.
10
17
 
11
18
  ## Homebrew Release Process
12
19
 
data/Rakefile CHANGED
@@ -46,3 +46,9 @@ end
46
46
 
47
47
  desc("Builds all distribution packages of the CLI")
48
48
  task(package: 'package:all')
49
+
50
+ desc("Update files in the repo to match new version")
51
+ task :prerelease, [:version] do |_t, args|
52
+ require 'theme_check/releaser'
53
+ ThemeCheck::Releaser.new.release(args.version)
54
+ end
data/config/default.yml CHANGED
@@ -93,3 +93,6 @@ AssetSizeCSS:
93
93
 
94
94
  ImgWidthAndHeight:
95
95
  enabled: true
96
+
97
+ RemoteAsset:
98
+ enabled: true
@@ -0,0 +1,82 @@
1
+ # Discourage use of third party domains for hosting assets (`RemoteAsset`)
2
+
3
+ Years ago, loading jQuery from a common CDN was good for performance because the browser cache could be reused across website. This is no longer true because browsers now include the domain from which the request was made in the cache key.
4
+
5
+ Therefore, this technique now makes things worse. Here's why:
6
+
7
+ * **The benefits of HTTP/2 prioritization are lost.** HTTP/2 prioritization is a mechanism used by servers. If different servers are used to deliver assets, there's no way to prioritize.
8
+ * **A new connection dance (DNS, TCP, TLS) must be done to start downloading the resource.** With HTTPS, this takes 5 round trips to achieve. The farther away the buyer is from that domain, the longer it takes.
9
+ * **The [slow start][slowstart] part of the Internet's TCP congestion control strategy must happen on every connection.** This means that the download "acceleration" we commonly observe must be repeated many times over.
10
+
11
+ The fix? Deliver as much as you can from a small number of connections. In a Shopify context, this is done by leveraging the `assets/` folder and the [URL filters][url_filters].
12
+
13
+ ## Check Details
14
+
15
+ This check is aimed at eliminating unnecessary HTTP connections.
16
+
17
+ :-1: Examples of **incorrect** code for this check:
18
+
19
+ ```liquid
20
+ <!-- Using multiple CDNs -->
21
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" defer></script>
22
+ {{ "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" | stylesheet_tag }}
23
+ <img src="https://example.com/heart.png" ...>
24
+
25
+ <!-- Missing img_url filter -->
26
+ <img src="{{ image }}" ...>
27
+ ```
28
+
29
+ In the examples above, multiple connections are competing for resources, are accelerating download independently and are improperly prioritized.
30
+
31
+ :+1: Examples of **correct** code for this check:
32
+
33
+ ```liquid
34
+ <!-- Good -->
35
+ <script src="{{ 'jquery.min.js' | asset_url }}" defer></script>
36
+ {{ 'bootstrap.min.css' | asset_url | stylesheet_tag }}
37
+
38
+ <!-- Better -->
39
+ <script src="{{ 'theme.js' | asset_url }}" defer></script>
40
+ {{ 'theme.css' | asset_url | stylesheet_tag }}
41
+
42
+ <!-- Images -->
43
+ <img src="{{ image | img_url }}" ...>
44
+ ```
45
+
46
+ In the above, the JavaScript, CSS and images are all loading from the same connection. Making it so the browser and CDN can properly prioritize which assets are downloaded first while maintaining a "hot" connection that downloads fast.
47
+
48
+ This can be done by downloading the files from those CDNs directly into your theme's `assets/` folder.
49
+
50
+ Use the [`img_url` filter][img_url] for images.
51
+
52
+ ## Check Options
53
+
54
+ The default configuration for this check is the following:
55
+
56
+ ```yaml
57
+ RemoteAsset:
58
+ enabled: true
59
+ ```
60
+
61
+ ## When Not To Use It
62
+
63
+ When the remote content is highly dynamic.
64
+
65
+ ## Version
66
+
67
+ This check has been introduced in Theme Check 0.7.0.
68
+
69
+ ## Resources
70
+
71
+ - [Announcement by Google][googleprivacy]
72
+ - [HTTP Cache Partioning Explainer](https://github.com/shivanigithub/http-cache-partitioning)
73
+ - [Slow Start][slowstart]
74
+ - [Rule Source][codesource]
75
+ - [Documentation Source][docsource]
76
+
77
+ [googleprivacy]: https://developers.google.com/web/updates/2020/10/http-cache-partitioning#resources
78
+ [codesource]: /lib/theme_check/checks/remote_asset.rb
79
+ [docsource]: /docs/checks/remote_asset.md
80
+ [slowstart]: https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start
81
+ [url_filters]: https://shopify.dev/docs/themes/liquid/reference/filters/url-filters
82
+ [img_url]: https://shopify.dev/docs/themes/liquid/reference/filters/url-filters#img_url
data/lib/theme_check.rb CHANGED
@@ -22,6 +22,7 @@ require_relative "theme_check/offense"
22
22
  require_relative "theme_check/printer"
23
23
  require_relative "theme_check/shopify_liquid"
24
24
  require_relative "theme_check/storage"
25
+ require_relative "theme_check/string_helpers"
25
26
  require_relative "theme_check/file_system_storage"
26
27
  require_relative "theme_check/in_memory_storage"
27
28
  require_relative "theme_check/tags"
@@ -82,7 +82,7 @@ module ThemeCheck
82
82
  end
83
83
 
84
84
  def code_name
85
- self.class.name.demodulize
85
+ StringHelpers.demodulize(self.class.name)
86
86
  end
87
87
 
88
88
  def ignore!
@@ -75,7 +75,7 @@ module ThemeCheck
75
75
  # asset_url (+ optional stylesheet_tag) variables
76
76
  if href =~ /^#{VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
77
77
  asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
78
- asset = @theme.assets.find { |a| a.name.ends_with?("/" + asset_id) }
78
+ asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
79
79
  return if asset.nil?
80
80
  asset.gzipped_size
81
81
 
@@ -56,7 +56,7 @@ module ThemeCheck
56
56
  # More complicated liquid statements are not in scope.
57
57
  if src =~ /^#{VARIABLE}$/o && src =~ /asset_url/ && src =~ Liquid::QuotedString
58
58
  asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
59
- asset = @theme.assets.find { |a| a.name.ends_with?("/" + asset_id) }
59
+ asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
60
60
  return if asset.nil?
61
61
  asset.gzipped_size
62
62
  elsif src =~ %r{^(https?:)?//}
@@ -8,7 +8,7 @@ module ThemeCheck
8
8
  doc docs_url(__FILE__)
9
9
 
10
10
  # Not implemented with lookbehinds and lookaheads because performance was shit!
11
- IMG_TAG = /<img#{HTML_ATTRIBUTES}>/oxim
11
+ IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
12
12
  SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
13
13
  WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
14
14
  HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
@@ -35,7 +35,7 @@ module ThemeCheck
35
35
  def record_missing_field_offenses(img_match)
36
36
  width = WIDTH_ATTRIBUTE.match(img_match[0])
37
37
  height = HEIGHT_ATTRIBUTE.match(img_match[0])
38
- return if width.present? && height.present?
38
+ return if width && height
39
39
  missing_width = width.nil?
40
40
  missing_height = height.nil?
41
41
  error_message = if missing_width && missing_height
@@ -11,7 +11,7 @@ module ThemeCheck
11
11
  end
12
12
 
13
13
  def on_file(file)
14
- return unless file.name.starts_with?("locales/")
14
+ return unless file.name.start_with?("locales/")
15
15
  return unless file.content.is_a?(Hash)
16
16
  return if file.name == @theme.default_locale_json&.name
17
17
 
@@ -11,7 +11,7 @@ module ThemeCheck
11
11
  <script # Find the start of a script tag
12
12
  (?=[^>]+?src=) # Make sure src= is in the script with a lookahead
13
13
  (?:(?!defer|async|type=["']module['"]).)*? # Find tags that don't have defer|async|type="module"
14
- >
14
+ /?>
15
15
  }xim
16
16
  SCRIPT_TAG_FILTER = /\{\{[^}]+script_tag\s+\}\}/
17
17
 
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ class RemoteAsset < LiquidCheck
4
+ include RegexHelpers
5
+ severity :suggestion
6
+ categories :liquid, :performance
7
+ doc docs_url(__FILE__)
8
+
9
+ OFFENSE_MESSAGE = "Asset should be served by the Shopify CDN for better performance."
10
+
11
+ HTML_FILTERS = [
12
+ 'stylesheet_tag',
13
+ 'script_tag',
14
+ 'img_tag',
15
+ ]
16
+ ASSET_URL_FILTERS = [
17
+ 'asset_url',
18
+ 'asset_img_url',
19
+ 'file_img_url',
20
+ 'file_url',
21
+ 'global_asset_url',
22
+ 'img_url',
23
+ 'payment_type_img_url',
24
+ 'shopify_asset_url',
25
+ ]
26
+
27
+ RESOURCE_TAG = /<(?<tag_name>img|script|link|source)#{HTML_ATTRIBUTES}>/oim
28
+ RESOURCE_URL = /\s(?:src|href)=(?<resource_url>#{QUOTED_LIQUID_ATTRIBUTE})/oim
29
+ ASSET_URL_FILTER = /[\|\s]*(#{ASSET_URL_FILTERS.join('|')})/omi
30
+ PROTOCOL = %r{(https?:)?//}
31
+ ABSOLUTE_PATH = %r{\A/[^/]}im
32
+ RELATIVE_PATH = %r{\A(?!#{PROTOCOL})[^/\{]}oim
33
+ REL = /\srel=(?<rel>#{QUOTED_LIQUID_ATTRIBUTE})/oim
34
+
35
+ def on_variable(node)
36
+ record_variable_offense(node)
37
+ end
38
+
39
+ def on_document(node)
40
+ source = node.template.source
41
+ record_html_offenses(node, source)
42
+ end
43
+
44
+ private
45
+
46
+ def record_variable_offense(variable_node)
47
+ # We flag HTML tags with URLs not hosted by Shopify
48
+ return if !html_resource_drop?(variable_node) || variable_hosted_by_shopify?(variable_node)
49
+ add_offense(OFFENSE_MESSAGE, node: variable_node)
50
+ end
51
+
52
+ def html_resource_drop?(variable_node)
53
+ variable_node.value.filters
54
+ .any? { |(filter_name, *_filter_args)| HTML_FILTERS.include?(filter_name) }
55
+ end
56
+
57
+ def variable_hosted_by_shopify?(variable_node)
58
+ variable_node.value.filters
59
+ .any? { |(filter_name, *_filter_args)| ASSET_URL_FILTERS.include?(filter_name) }
60
+ end
61
+
62
+ # This part is slightly more complicated because we don't have an
63
+ # HTML AST. We have to resort to looking at the HTML with regexes
64
+ # to figure out if we have a resource (stylesheet, script, or media)
65
+ # that points to a remote domain.
66
+ def record_html_offenses(node, source)
67
+ matches(source, RESOURCE_TAG).each do |match|
68
+ tag = match[0]
69
+
70
+ # We don't flag stuff without URLs
71
+ next unless tag =~ RESOURCE_URL
72
+ resource_match = Regexp.last_match
73
+ resource_url = resource_match[:resource_url].gsub(START_OR_END_QUOTE, '')
74
+
75
+ next if non_stylesheet_link?(tag)
76
+ next if url_hosted_by_shopify?(resource_url)
77
+ next if resource_url =~ ABSOLUTE_PATH
78
+ next if resource_url =~ RELATIVE_PATH
79
+
80
+ start = match.begin(0) + resource_match.begin(:resource_url)
81
+ add_offense(
82
+ OFFENSE_MESSAGE,
83
+ node: node,
84
+ markup: resource_url,
85
+ line_number: source[0...start].count("\n") + 1,
86
+ )
87
+ end
88
+ end
89
+
90
+ def non_stylesheet_link?(tag)
91
+ tag =~ REL && !(Regexp.last_match[:rel] =~ /\A['"]stylesheet['"]\Z/)
92
+ end
93
+
94
+ def url_hosted_by_shopify?(url)
95
+ url =~ /\A#{VARIABLE}\Z/oim && url =~ ASSET_URL_FILTER
96
+ end
97
+ end
98
+ end
@@ -4,10 +4,7 @@ module ThemeCheck
4
4
  extend self
5
5
 
6
6
  def translations
7
- @translations ||= begin
8
- # loaded as a Set because the include? lookup will be much faster.
9
- YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
10
- end
7
+ @translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
11
8
  end
12
9
 
13
10
  def include?(key)
@@ -135,7 +135,7 @@ module ThemeCheck
135
135
 
136
136
  def each_template
137
137
  @files.each do |(name, info)|
138
- next if name.starts_with?('snippets/')
138
+ next if name.start_with?('snippets/')
139
139
  yield [name, info]
140
140
  end
141
141
  end
@@ -8,7 +8,7 @@ module ThemeCheck
8
8
  doc docs_url(__FILE__)
9
9
 
10
10
  def on_file(file)
11
- return unless file.name.starts_with?("locales/")
11
+ return unless file.name.start_with?("locales/")
12
12
  return unless file.content.is_a?(Hash)
13
13
 
14
14
  visit_nested(file.content)
@@ -6,14 +6,17 @@ module ThemeCheck
6
6
  USAGE = <<~END
7
7
  Usage: theme-check [options] /path/to/your/theme
8
8
 
9
- Options:
10
- --init Generate a .theme-check.yml file in the current directory
9
+ Basic Options:
11
10
  -C, --config <path> Use the config provided, overriding .theme-check.yml if present
12
11
  -c, --category <category> Only run this category of checks
13
12
  -x, --exclude-category <category> Exclude this category of checks
14
- -l, --list List enabled checks
15
13
  -a, --auto-correct Automatically fix offenses
14
+
15
+ Miscellaneous:
16
+ --init Generate a .theme-check.yml file
17
+ --print-config Output active config to STDOUT
16
18
  -h, --help Show this. Hi!
19
+ -l, --list List enabled checks
17
20
  -v, --version Print Theme Check version
18
21
 
19
22
  Description:
@@ -51,13 +54,15 @@ module ThemeCheck
51
54
  auto_correct = true
52
55
  when "--init"
53
56
  command = :init
57
+ when "--print"
58
+ command = :print
54
59
  else
55
60
  @path = arg
56
61
  end
57
62
  end
58
63
 
59
64
  unless [:version, :init].include?(command)
60
- @config = if config_path.present?
65
+ @config = if config_path
61
66
  ThemeCheck::Config.new(
62
67
  root: @path,
63
68
  configuration: ThemeCheck::Config.load_file(config_path)
@@ -102,6 +107,10 @@ module ThemeCheck
102
107
  end
103
108
  end
104
109
 
110
+ def print
111
+ puts YAML.dump(@config.to_h)
112
+ end
113
+
105
114
  def check
106
115
  puts "Checking #{@config.root} ..."
107
116
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
@@ -61,11 +61,11 @@ module ThemeCheck
61
61
  end
62
62
 
63
63
  def start_disabling?(text)
64
- text.strip.starts_with?(DISABLE_START)
64
+ text.strip.start_with?(DISABLE_START)
65
65
  end
66
66
 
67
67
  def stop_disabling?(text)
68
- text.strip.starts_with?(DISABLE_END)
68
+ text.strip.start_with?(DISABLE_END)
69
69
  end
70
70
 
71
71
  # Return a list of checks from a theme-check-disable comment
@@ -6,32 +6,25 @@
6
6
  # as a big hash already, leave it like that and save yourself some IO.
7
7
  module ThemeCheck
8
8
  class InMemoryStorage < Storage
9
- def initialize(files = {}, root = nil)
9
+ def initialize(files = {}, root = "/dev/null")
10
10
  @files = files
11
- @root = root
11
+ @root = Pathname.new(root)
12
12
  end
13
13
 
14
- def path(name)
15
- return File.join(@root, name) unless @root.nil?
16
- name
14
+ def path(relative_path)
15
+ @root.join(relative_path)
17
16
  end
18
17
 
19
- def relative_path(name)
20
- path = Pathname.new(name)
21
- return path.relative_path_from(@root).to_s unless path.relative? || @root.nil?
22
- name
18
+ def read(relative_path)
19
+ @files[relative_path]
23
20
  end
24
21
 
25
- def read(name)
26
- @files[relative_path(name)]
27
- end
28
-
29
- def write(name, content)
30
- @files[relative_path(name)] = content
22
+ def write(relative_path, content)
23
+ @files[relative_path] = content
31
24
  end
32
25
 
33
26
  def files
34
- @values ||= @files.keys
27
+ @files.keys
35
28
  end
36
29
 
37
30
  def directories
@@ -41,5 +34,9 @@ module ThemeCheck
41
34
  .map(&:to_s)
42
35
  .uniq
43
36
  end
37
+
38
+ def relative_path(absolute_path)
39
+ Pathname.new(absolute_path).relative_path_from(@root).to_s
40
+ end
44
41
  end
45
42
  end
@@ -10,8 +10,8 @@ module ThemeCheck
10
10
  @providers = CompletionProvider.all.map { |x| x.new(storage) }
11
11
  end
12
12
 
13
- def completions(name, line, col)
14
- buffer = @storage.read(name)
13
+ def completions(relative_path, line, col)
14
+ buffer = @storage.read(relative_path)
15
15
  cursor = from_line_column_to_index(buffer, line, col)
16
16
  token = find_token(buffer, cursor)
17
17
  return [] if token.nil?
@@ -8,7 +8,7 @@ module ThemeCheck
8
8
  def completions(content, cursor)
9
9
  return [] unless can_complete?(content, cursor)
10
10
  available_labels
11
- .select { |w| w.starts_with?(partial(content, cursor)) }
11
+ .select { |w| w.start_with?(partial(content, cursor)) }
12
12
  .map { |filter| filter_to_completion(filter) }
13
13
  end
14
14
 
@@ -7,7 +7,7 @@ module ThemeCheck
7
7
  return [] unless can_complete?(content, cursor)
8
8
  partial = first_word(content) || ''
9
9
  ShopifyLiquid::Object.labels
10
- .select { |w| w.starts_with?(partial) }
10
+ .select { |w| w.start_with?(partial) }
11
11
  .map { |object| object_to_completion(object) }
12
12
  end
13
13
 
@@ -7,7 +7,7 @@ module ThemeCheck
7
7
  return [] unless cursor_on_quoted_argument?(content, cursor)
8
8
  partial = snippet(content) || ''
9
9
  snippets
10
- .select { |x| x.starts_with?(partial) }
10
+ .select { |x| x.start_with?(partial) }
11
11
  .map { |x| snippet_to_completion(x) }
12
12
  end
13
13
 
@@ -7,12 +7,12 @@ module ThemeCheck
7
7
  return [] unless can_complete?(content, cursor)
8
8
  partial = first_word(content) || ''
9
9
  ShopifyLiquid::Tag.labels
10
- .select { |w| w.starts_with?(partial) }
10
+ .select { |w| w.start_with?(partial) }
11
11
  .map { |tag| tag_to_completion(tag) }
12
12
  end
13
13
 
14
14
  def can_complete?(content, cursor)
15
- content.starts_with?(Liquid::TagStart) && (
15
+ content.start_with?(Liquid::TagStart) && (
16
16
  cursor_on_first_word?(content, cursor) ||
17
17
  cursor_on_start_content?(content, cursor, Liquid::TagStart)
18
18
  )
@@ -10,8 +10,9 @@ module ThemeCheck
10
10
  @storage = storage
11
11
  end
12
12
 
13
- def document_links(uri)
14
- buffer = @storage.read(uri)
13
+ def document_links(relative_path)
14
+ buffer = @storage.read(relative_path)
15
+ return [] unless buffer
15
16
  matches(buffer, PARTIAL_RENDER).map do |match|
16
17
  start_line, start_character = from_index_to_line_column(
17
18
  buffer,
@@ -40,7 +41,7 @@ module ThemeCheck
40
41
  end
41
42
 
42
43
  def link(partial)
43
- 'file://' + @storage.path('snippets/' + partial + '.liquid')
44
+ "file://#{@storage.path('snippets/' + partial + '.liquid')}"
44
45
  end
45
46
  end
46
47
  end
@@ -42,19 +42,19 @@ module ThemeCheck
42
42
  alias_method :on_shutdown, :on_exit
43
43
 
44
44
  def on_text_document_did_change(_id, params)
45
- uri = text_document_uri(params)
46
- @storage.write(uri, content_changes_text(params))
45
+ relative_path = relative_path_from_text_document_uri(params)
46
+ @storage.write(relative_path, content_changes_text(params))
47
47
  end
48
48
 
49
49
  def on_text_document_did_close(_id, params)
50
- uri = text_document_uri(params)
51
- @storage.write(uri, nil)
50
+ relative_path = relative_path_from_text_document_uri(params)
51
+ @storage.write(relative_path, "")
52
52
  end
53
53
 
54
54
  def on_text_document_did_open(_id, params)
55
- uri = text_document_uri(params)
56
- @storage.write(uri, text_document_text(params))
57
- analyze_and_send_offenses(uri)
55
+ relative_path = relative_path_from_text_document_uri(params)
56
+ @storage.write(relative_path, text_document_text(params))
57
+ analyze_and_send_offenses(text_document_uri(params))
58
58
  end
59
59
 
60
60
  def on_text_document_did_save(_id, params)
@@ -62,27 +62,27 @@ module ThemeCheck
62
62
  end
63
63
 
64
64
  def on_text_document_document_link(id, params)
65
- uri = text_document_uri(params)
65
+ relative_path = relative_path_from_text_document_uri(params)
66
66
  send_response(
67
67
  id: id,
68
- result: document_links(uri)
68
+ result: document_links(relative_path)
69
69
  )
70
70
  end
71
71
 
72
72
  def on_text_document_completion(id, params)
73
- uri = text_document_uri(params)
73
+ relative_path = relative_path_from_text_document_uri(params)
74
74
  line = params.dig('position', 'line')
75
75
  col = params.dig('position', 'character')
76
76
  send_response(
77
77
  id: id,
78
- result: completions(uri, line, col)
78
+ result: completions(relative_path, line, col)
79
79
  )
80
80
  end
81
81
 
82
82
  private
83
83
 
84
84
  def in_memory_storage(root)
85
- config = ThemeCheck::Config.from_path(root)
85
+ config = config_for_path(root)
86
86
 
87
87
  # Make a real FS to get the files from the snippets folder
88
88
  fs = ThemeCheck::FileSystemStorage.new(
@@ -95,13 +95,17 @@ module ThemeCheck
95
95
  .map { |fn| [fn, ""] }
96
96
  .to_h
97
97
 
98
- InMemoryStorage.new(files, root)
98
+ InMemoryStorage.new(files, config.root)
99
99
  end
100
100
 
101
101
  def text_document_uri(params)
102
102
  params.dig('textDocument', 'uri').sub('file://', '')
103
103
  end
104
104
 
105
+ def relative_path_from_text_document_uri(params)
106
+ @storage.relative_path(text_document_uri(params))
107
+ end
108
+
105
109
  def text_document_text(params)
106
110
  params.dig('textDocument', 'text')
107
111
  end
@@ -110,9 +114,13 @@ module ThemeCheck
110
114
  params.dig('contentChanges', 0, 'text')
111
115
  end
112
116
 
113
- def analyze_and_send_offenses(file_path)
114
- root = ThemeCheck::Config.find(file_path) || @root_path
115
- config = ThemeCheck::Config.from_path(root)
117
+ def config_for_path(path)
118
+ root = ThemeCheck::Config.find(path) || @root_path
119
+ ThemeCheck::Config.from_path(root)
120
+ end
121
+
122
+ def analyze_and_send_offenses(absolute_path)
123
+ config = config_for_path(absolute_path)
116
124
  storage = ThemeCheck::FileSystemStorage.new(
117
125
  config.root,
118
126
  ignored_patterns: config.ignored_patterns
@@ -131,12 +139,12 @@ module ThemeCheck
131
139
  analyzer.offenses
132
140
  end
133
141
 
134
- def completions(uri, line, col)
135
- @completion_engine.completions(uri, line, col)
142
+ def completions(relative_path, line, col)
143
+ @completion_engine.completions(relative_path, line, col)
136
144
  end
137
145
 
138
- def document_links(uri)
139
- @document_link_engine.document_links(uri)
146
+ def document_links(relative_path)
147
+ @document_link_engine.document_links(relative_path)
140
148
  end
141
149
 
142
150
  def send_diagnostics(offenses)
@@ -162,7 +170,7 @@ module ThemeCheck
162
170
  send_response(
163
171
  method: 'textDocument/publishDiagnostics',
164
172
  params: {
165
- uri: "file:#{path}",
173
+ uri: "file://#{path}",
166
174
  diagnostics: offenses.map { |offense| offense_to_diagnostic(offense) },
167
175
  },
168
176
  )
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'json'
3
3
  require 'stringio'
4
- require 'active_support/core_ext/string/inflections'
5
4
 
6
5
  module ThemeCheck
7
6
  module LanguageServer
@@ -99,7 +98,7 @@ module ThemeCheck
99
98
  end
100
99
 
101
100
  def to_snake_case(method_name)
102
- method_name.gsub(/[^\w]/, '_').underscore
101
+ StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
103
102
  end
104
103
 
105
104
  def initial_line
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'active_support/core_ext/string/inflections'
3
2
 
4
3
  module ThemeCheck
5
4
  # A node from the Liquid AST, the result of parsing a template.
@@ -101,7 +100,7 @@ module ThemeCheck
101
100
  # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
102
101
  # and `after_<type_name>` check methods.
103
102
  def type_name
104
- @type_name ||= @value.class.name.demodulize.underscore.to_sym
103
+ @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
105
104
  end
106
105
 
107
106
  # Is this node inside a `{% liquid ... %}` block?
@@ -83,7 +83,7 @@ module ThemeCheck
83
83
  end
84
84
 
85
85
  def check_name
86
- check.class.name.demodulize
86
+ StringHelpers.demodulize(check.class.name)
87
87
  end
88
88
 
89
89
  def doc
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'theme_check/version'
3
+
4
+ module ThemeCheck
5
+ class Releaser
6
+ ROOT = File.expand_path('../../..', __FILE__)
7
+ LIB = File.join(ROOT, 'lib')
8
+
9
+ class VersionError < StandardError; end
10
+
11
+ def release(version)
12
+ raise VersionError, "Missing version argument." if version.nil?
13
+ raise VersionError, "Version should be a string." unless version.is_a?(String)
14
+ raise VersionError, "Version should be a valid semver version." unless version =~ /^\d+\.\d+.\d+$/
15
+ update_docs(version)
16
+ update_version(version)
17
+ end
18
+
19
+ def update_version(version)
20
+ version_file_path = File.join(LIB, 'theme_check/version.rb')
21
+ version_file = File.read(version_file_path)
22
+ updated_version_file = version_file.gsub(ThemeCheck::VERSION, version)
23
+
24
+ return if updated_version_file == version_file
25
+ puts "Updating version to #{version} in #{version_file_path}."
26
+ File.write(version_file_path, updated_version_file)
27
+ end
28
+
29
+ def update_docs(version)
30
+ Dir[ROOT + '/docs/checks/*.md'].each do |filename|
31
+ doc_content = File.read(filename)
32
+ updated_doc_content = doc_content.gsub('THEME_CHECK_VERSION', version)
33
+ next if updated_doc_content == doc_content
34
+ puts "Replacing `THEME_CHECK_VERSION` with #{version} in #{Pathname.new(filename).relative_path_from(ROOT)}"
35
+ File.write(filename, updated_doc_content)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -17,14 +17,12 @@ module ThemeCheck
17
17
  private
18
18
 
19
19
  def all
20
- @all ||= begin
21
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/deprecated_filters.yml"))
22
- .values
23
- .each_with_object({}) do |filters, acc|
24
- filters.each do |(filter, alternatives)|
25
- acc[filter] = alternatives
26
- end
27
- end
20
+ @all ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/deprecated_filters.yml"))
21
+ .values
22
+ .each_with_object({}) do |filters, acc|
23
+ filters.each do |(filter, alternatives)|
24
+ acc[filter] = alternatives
25
+ end
28
26
  end
29
27
  end
30
28
  end
@@ -7,11 +7,9 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @labels ||= begin
11
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/filters.yml"))
12
- .values
13
- .flatten
14
- end
10
+ @labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/filters.yml"))
11
+ .values
12
+ .flatten
15
13
  end
16
14
  end
17
15
  end
@@ -7,15 +7,11 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @labels ||= begin
11
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/objects.yml"))
12
- end
10
+ @labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/objects.yml"))
13
11
  end
14
12
 
15
13
  def plus_labels
16
- @plus_labels ||= begin
17
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/plus_objects.yml"))
18
- end
14
+ @plus_labels ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/plus_objects.yml"))
19
15
  end
20
16
  end
21
17
  end
@@ -7,9 +7,7 @@ module ThemeCheck
7
7
  extend self
8
8
 
9
9
  def labels
10
- @tags ||= begin
11
- YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
12
- end
10
+ @tags ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_liquid/tags.yml"))
13
11
  end
14
12
  end
15
13
  end
@@ -2,15 +2,15 @@
2
2
 
3
3
  module ThemeCheck
4
4
  class Storage
5
- def read(relative_path)
5
+ def path(relative_path)
6
6
  raise NotImplementedError
7
7
  end
8
8
 
9
- def write(relative_path, content)
9
+ def read(relative_path)
10
10
  raise NotImplementedError
11
11
  end
12
12
 
13
- def path(relative_path)
13
+ def write(relative_path, content)
14
14
  raise NotImplementedError
15
15
  end
16
16
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module StringHelpers
5
+ extend self
6
+
7
+ # Removes the module part from the expression in the string.
8
+ # Ported from ActiveSupport
9
+ #
10
+ # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
11
+ # demodulize('Inflections') # => "Inflections"
12
+ # demodulize('::Inflections') # => "Inflections"
13
+ # demodulize('') # => ""
14
+ #
15
+ # See also #deconstantize.
16
+ def demodulize(path)
17
+ path = path.to_s
18
+ if (i = path.rindex("::"))
19
+ path[(i + 2)..-1]
20
+ else
21
+ path
22
+ end
23
+ end
24
+
25
+ # Makes an underscored, lowercase form from the expression in the string.
26
+ # Base on ActiveSupport's
27
+ #
28
+ # Changes '::' to '/' to convert namespaces to paths.
29
+ #
30
+ # underscore('ActiveModel') # => "active_model"
31
+ # underscore('ActiveModel::Errors') # => "active_model/errors"
32
+ #
33
+ # As a rule of thumb you can think of +underscore+ as the inverse of
34
+ # #camelize, though there are cases where that does not hold:
35
+ #
36
+ # camelize(underscore('SSLError')) # => "SslError"
37
+ def underscore(camel_cased_word)
38
+ return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
39
+ word = camel_cased_word.to_s.gsub("::", "/")
40
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
41
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
42
+ word.tr!("-", "_")
43
+ word.downcase!
44
+ word
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require "active_support/core_ext/string/starts_ends_with"
3
2
 
4
3
  module ThemeCheck
5
4
  module Tags
@@ -19,7 +18,7 @@ module ThemeCheck
19
18
  "Error in tag 'section' - Valid syntax: section '[type]'",
20
19
  ) unless match
21
20
  @section_name = match[:section_name].tr(%('"), '')
22
- @section_name.chomp!(".liquid") if @section_name.ends_with?(".liquid")
21
+ @section_name.chomp!(".liquid") if @section_name.end_with?(".liquid")
23
22
  end
24
23
  end
25
24
 
@@ -13,7 +13,7 @@ module ThemeCheck
13
13
 
14
14
  def assets
15
15
  @assets ||= @storage.files
16
- .select { |path| path.starts_with?("assets/") }
16
+ .select { |path| path.start_with?("assets/") }
17
17
  .map { |path| AssetFile.new(path, @storage) }
18
18
  end
19
19
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "0.6.0"
3
+ VERSION = "0.7.0"
4
4
  end
data/theme-check.gemspec CHANGED
@@ -22,7 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency('liquid', '>= 5')
26
- spec.add_dependency('activesupport')
25
+ spec.add_dependency('liquid', '>= 5.0.1')
27
26
  spec.add_dependency('nokogumbo')
28
27
  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.6.0
4
+ version: 0.7.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-03-23 00:00:00.000000000 Z
11
+ date: 2021-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: 5.0.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
27
- - !ruby/object:Gem::Dependency
28
- name: activesupport
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
26
+ version: 5.0.1
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: nokogumbo
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +84,7 @@ files:
98
84
  - docs/checks/missing_template.md
99
85
  - docs/checks/nested_snippet.md
100
86
  - docs/checks/parser_blocking_javascript.md
87
+ - docs/checks/remote_asset.md
101
88
  - docs/checks/required_directories.md
102
89
  - docs/checks/required_layout_theme_object.md
103
90
  - docs/checks/space_inside_braces.md
@@ -133,6 +120,7 @@ files:
133
120
  - lib/theme_check/checks/missing_template.rb
134
121
  - lib/theme_check/checks/nested_snippet.rb
135
122
  - lib/theme_check/checks/parser_blocking_javascript.rb
123
+ - lib/theme_check/checks/remote_asset.rb
136
124
  - lib/theme_check/checks/required_directories.rb
137
125
  - lib/theme_check/checks/required_layout_theme_object.rb
138
126
  - lib/theme_check/checks/space_inside_braces.rb
@@ -179,6 +167,7 @@ files:
179
167
  - lib/theme_check/parsing_helpers.rb
180
168
  - lib/theme_check/printer.rb
181
169
  - lib/theme_check/regex_helpers.rb
170
+ - lib/theme_check/releaser.rb
182
171
  - lib/theme_check/remote_asset_file.rb
183
172
  - lib/theme_check/shopify_liquid.rb
184
173
  - lib/theme_check/shopify_liquid/deprecated_filter.rb
@@ -186,6 +175,7 @@ files:
186
175
  - lib/theme_check/shopify_liquid/object.rb
187
176
  - lib/theme_check/shopify_liquid/tag.rb
188
177
  - lib/theme_check/storage.rb
178
+ - lib/theme_check/string_helpers.rb
189
179
  - lib/theme_check/tags.rb
190
180
  - lib/theme_check/template.rb
191
181
  - lib/theme_check/theme.rb