theme-check 0.4.0 → 0.7.2

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +10 -3
  3. data/.rubocop.yml +12 -3
  4. data/CHANGELOG.md +40 -0
  5. data/CONTRIBUTING.md +5 -2
  6. data/Gemfile +5 -3
  7. data/LICENSE.md +2 -0
  8. data/README.md +12 -2
  9. data/RELEASING.md +10 -3
  10. data/Rakefile +6 -0
  11. data/config/default.yml +16 -0
  12. data/data/shopify_liquid/tags.yml +1 -0
  13. data/data/shopify_translation_keys.yml +850 -0
  14. data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
  15. data/docs/checks/asset_size_css.md +52 -0
  16. data/docs/checks/asset_size_javascript.md +79 -0
  17. data/docs/checks/convert_include_to_render.md +48 -0
  18. data/docs/checks/default_locale.md +46 -0
  19. data/docs/checks/deprecated_filter.md +46 -0
  20. data/docs/checks/img_width_and_height.md +79 -0
  21. data/docs/checks/liquid_tag.md +65 -0
  22. data/docs/checks/matching_schema_translations.md +93 -0
  23. data/docs/checks/matching_translations.md +72 -0
  24. data/docs/checks/missing_enable_comment.md +50 -0
  25. data/docs/checks/missing_required_template_files.md +26 -0
  26. data/docs/checks/missing_template.md +40 -0
  27. data/docs/checks/nested_snippet.md +69 -0
  28. data/docs/checks/parser_blocking_javascript.md +97 -0
  29. data/docs/checks/remote_asset.md +82 -0
  30. data/docs/checks/required_directories.md +25 -0
  31. data/docs/checks/required_layout_theme_object.md +28 -0
  32. data/docs/checks/space_inside_braces.md +63 -0
  33. data/docs/checks/syntax_error.md +49 -0
  34. data/docs/checks/template_length.md +50 -0
  35. data/docs/checks/translation_key_exists.md +63 -0
  36. data/docs/checks/undefined_object.md +53 -0
  37. data/docs/checks/unknown_filter.md +45 -0
  38. data/docs/checks/unused_assign.md +47 -0
  39. data/docs/checks/unused_snippet.md +32 -0
  40. data/docs/checks/valid_html_translation.md +53 -0
  41. data/docs/checks/valid_json.md +60 -0
  42. data/docs/checks/valid_schema.md +50 -0
  43. data/lib/theme_check.rb +4 -0
  44. data/lib/theme_check/asset_file.rb +34 -0
  45. data/lib/theme_check/check.rb +20 -10
  46. data/lib/theme_check/checks/asset_size_css.rb +89 -0
  47. data/lib/theme_check/checks/asset_size_javascript.rb +68 -0
  48. data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
  49. data/lib/theme_check/checks/default_locale.rb +1 -0
  50. data/lib/theme_check/checks/deprecated_filter.rb +1 -1
  51. data/lib/theme_check/checks/img_width_and_height.rb +74 -0
  52. data/lib/theme_check/checks/liquid_tag.rb +3 -3
  53. data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
  54. data/lib/theme_check/checks/matching_translations.rb +2 -1
  55. data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
  56. data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
  57. data/lib/theme_check/checks/missing_template.rb +1 -0
  58. data/lib/theme_check/checks/nested_snippet.rb +1 -0
  59. data/lib/theme_check/checks/parser_blocking_javascript.rb +8 -15
  60. data/lib/theme_check/checks/remote_asset.rb +99 -0
  61. data/lib/theme_check/checks/required_directories.rb +1 -1
  62. data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
  63. data/lib/theme_check/checks/space_inside_braces.rb +1 -0
  64. data/lib/theme_check/checks/syntax_error.rb +1 -0
  65. data/lib/theme_check/checks/template_length.rb +1 -0
  66. data/lib/theme_check/checks/translation_key_exists.rb +14 -1
  67. data/lib/theme_check/checks/undefined_object.rb +11 -5
  68. data/lib/theme_check/checks/unknown_filter.rb +1 -0
  69. data/lib/theme_check/checks/unused_assign.rb +1 -0
  70. data/lib/theme_check/checks/unused_snippet.rb +1 -0
  71. data/lib/theme_check/checks/valid_html_translation.rb +2 -1
  72. data/lib/theme_check/checks/valid_json.rb +1 -0
  73. data/lib/theme_check/checks/valid_schema.rb +1 -0
  74. data/lib/theme_check/cli.rb +29 -9
  75. data/lib/theme_check/config.rb +5 -2
  76. data/lib/theme_check/disabled_checks.rb +2 -2
  77. data/lib/theme_check/in_memory_storage.rb +13 -8
  78. data/lib/theme_check/language_server.rb +2 -0
  79. data/lib/theme_check/language_server/completion_engine.rb +3 -3
  80. data/lib/theme_check/language_server/completion_helper.rb +0 -10
  81. data/lib/theme_check/language_server/completion_provider.rb +5 -0
  82. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +6 -2
  83. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
  84. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
  85. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +2 -2
  86. data/lib/theme_check/language_server/constants.rb +10 -0
  87. data/lib/theme_check/language_server/document_link_engine.rb +48 -0
  88. data/lib/theme_check/language_server/handler.rb +67 -20
  89. data/lib/theme_check/language_server/server.rb +9 -4
  90. data/lib/theme_check/liquid_check.rb +11 -0
  91. data/lib/theme_check/node.rb +1 -2
  92. data/lib/theme_check/offense.rb +3 -1
  93. data/lib/theme_check/packager.rb +1 -1
  94. data/lib/theme_check/regex_helpers.rb +15 -0
  95. data/lib/theme_check/releaser.rb +39 -0
  96. data/lib/theme_check/remote_asset_file.rb +44 -0
  97. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +10 -8
  98. data/lib/theme_check/shopify_liquid/filter.rb +3 -5
  99. data/lib/theme_check/shopify_liquid/object.rb +2 -6
  100. data/lib/theme_check/shopify_liquid/tag.rb +1 -3
  101. data/lib/theme_check/storage.rb +3 -3
  102. data/lib/theme_check/string_helpers.rb +47 -0
  103. data/lib/theme_check/tags.rb +1 -2
  104. data/lib/theme_check/theme.rb +7 -1
  105. data/lib/theme_check/version.rb +1 -1
  106. data/packaging/homebrew/theme_check.base.rb +1 -1
  107. data/theme-check.gemspec +1 -2
  108. metadata +46 -18
@@ -1,8 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
+ module SystemTranslations
4
+ extend self
5
+
6
+ def translations
7
+ @translations ||= YAML.load(File.read("#{__dir__}/../../../data/shopify_translation_keys.yml")).to_set
8
+ end
9
+
10
+ def include?(key)
11
+ translations.include?(key)
12
+ end
13
+ end
14
+
3
15
  class TranslationKeyExists < LiquidCheck
4
16
  severity :error
5
17
  category :translation
18
+ doc docs_url(__FILE__)
6
19
 
7
20
  def on_variable(node)
8
21
  return unless @theme.default_locale_json&.content&.is_a?(Hash)
@@ -11,7 +24,7 @@ module ThemeCheck
11
24
  return unless (key_node = node.children.first)
12
25
  return unless key_node.value.is_a?(String)
13
26
 
14
- unless key_exists?(key_node.value)
27
+ unless key_exists?(key_node.value) || SystemTranslations.include?(key_node.value)
15
28
  add_offense(
16
29
  "'#{key_node.value}' does not have a matching entry in '#{@theme.default_locale_json.relative_path}'",
17
30
  node: node,
@@ -2,7 +2,7 @@
2
2
  module ThemeCheck
3
3
  class UndefinedObject < LiquidCheck
4
4
  category :liquid
5
- doc "https://shopify.dev/docs/themes/liquid/reference/objects"
5
+ doc docs_url(__FILE__)
6
6
  severity :error
7
7
 
8
8
  class TemplateInfo
@@ -21,7 +21,13 @@ module ThemeCheck
21
21
  end
22
22
 
23
23
  def add_variable_lookup(name:, node:)
24
- line_number = node.parent.line_number
24
+ parent = node
25
+ line_number = nil
26
+ loop do
27
+ line_number = parent.line_number
28
+ parent = parent.parent
29
+ break unless line_number.nil? && parent
30
+ end
25
31
  key = [name, line_number]
26
32
  @all_variable_lookups[key] = node
27
33
  end
@@ -129,7 +135,7 @@ module ThemeCheck
129
135
 
130
136
  def each_template
131
137
  @files.each do |(name, info)|
132
- next if name.starts_with?('snippets/')
138
+ next if name.start_with?('snippets/')
133
139
  yield [name, info]
134
140
  end
135
141
  end
@@ -154,7 +160,7 @@ module ThemeCheck
154
160
  all_variables = info.all_variables
155
161
 
156
162
  info.each_variable_lookup(!!render_node) do |(key, node)|
157
- name, _line_number = key
163
+ name, line_number = key
158
164
  next if all_variables.include?(name)
159
165
  next if all_global_objects.include?(name)
160
166
 
@@ -164,7 +170,7 @@ module ThemeCheck
164
170
  if render_node
165
171
  add_offense("Missing argument `#{name}`", node: render_node)
166
172
  else
167
- add_offense("Undefined object `#{name}`", node: node)
173
+ add_offense("Undefined object `#{name}`", node: node, line_number: line_number)
168
174
  end
169
175
  end
170
176
  end
@@ -12,6 +12,7 @@ module ThemeCheck
12
12
  class UnknownFilter < LiquidCheck
13
13
  severity :error
14
14
  category :liquid
15
+ doc docs_url(__FILE__)
15
16
 
16
17
  def on_variable(node)
17
18
  used_filters = node.value.filters.map { |name, *_rest| name }
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class UnusedAssign < LiquidCheck
5
5
  severity :suggestion
6
6
  category :liquid
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  class TemplateInfo < Struct.new(:used_assigns, :assign_nodes, :includes)
9
10
  def collect_used_assigns(templates, visited = Set.new)
@@ -5,6 +5,7 @@ module ThemeCheck
5
5
  class UnusedSnippet < LiquidCheck
6
6
  severity :suggestion
7
7
  category :liquid
8
+ doc docs_url(__FILE__)
8
9
 
9
10
  def initialize
10
11
  @used_templates = Set.new
@@ -5,9 +5,10 @@ require 'nokogumbo'
5
5
  module ThemeCheck
6
6
  class ValidHTMLTranslation < JsonCheck
7
7
  severity :suggestion
8
+ doc docs_url(__FILE__)
8
9
 
9
10
  def on_file(file)
10
- return unless file.name.starts_with?("locales/")
11
+ return unless file.name.start_with?("locales/")
11
12
  return unless file.content.is_a?(Hash)
12
13
 
13
14
  visit_nested(file.content)
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class ValidJson < JsonCheck
4
4
  severity :error
5
5
  category :json
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def on_file(file)
8
9
  if file.parse_error
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class ValidSchema < LiquidCheck
4
4
  severity :suggestion
5
5
  category :json
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def on_schema(node)
8
9
  JSON.parse(node.value.nodelist.join)
@@ -6,14 +6,18 @@ module ThemeCheck
6
6
  USAGE = <<~END
7
7
  Usage: theme-check [options] /path/to/your/theme
8
8
 
9
- Options:
10
- -c, [--category] # Only run this category of checks
11
- -x, [--exclude-category] # Exclude this category of checks
12
- -l, [--list] # List enabled checks
13
- -a, [--auto-correct] # Automatically fix offenses
14
- --init # Generate a .theme-check.yml file in the current directory
15
- -h, [--help] # Show this. Hi!
16
- -v, [--version] # Print Theme Check version
9
+ Basic Options:
10
+ -C, --config <path> Use the config provided, overriding .theme-check.yml if present
11
+ -c, --category <category> Only run this category of checks
12
+ -x, --exclude-category <category> Exclude this category of checks
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
18
+ -h, --help Show this. Hi!
19
+ -l, --list List enabled checks
20
+ -v, --version Print Theme Check version
17
21
 
18
22
  Description:
19
23
  Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
@@ -29,6 +33,7 @@ module ThemeCheck
29
33
  only_categories = []
30
34
  exclude_categories = []
31
35
  auto_correct = false
36
+ config_path = nil
32
37
 
33
38
  args = argv.dup
34
39
  while (arg = args.shift)
@@ -37,6 +42,8 @@ module ThemeCheck
37
42
  raise Abort, USAGE
38
43
  when "--version", "-v"
39
44
  command = :version
45
+ when "--config", "-C"
46
+ config_path = Pathname.new(args.shift)
40
47
  when "--category", "-c"
41
48
  only_categories << args.shift.to_sym
42
49
  when "--exclude-category", "-x"
@@ -47,13 +54,22 @@ module ThemeCheck
47
54
  auto_correct = true
48
55
  when "--init"
49
56
  command = :init
57
+ when "--print"
58
+ command = :print
50
59
  else
51
60
  @path = arg
52
61
  end
53
62
  end
54
63
 
55
64
  unless [:version, :init].include?(command)
56
- @config = ThemeCheck::Config.from_path(@path)
65
+ @config = if config_path
66
+ ThemeCheck::Config.new(
67
+ root: @path,
68
+ configuration: ThemeCheck::Config.load_file(config_path)
69
+ )
70
+ else
71
+ ThemeCheck::Config.from_path(@path)
72
+ end
57
73
  @config.only_categories = only_categories
58
74
  @config.exclude_categories = exclude_categories
59
75
  @config.auto_correct = auto_correct
@@ -91,6 +107,10 @@ module ThemeCheck
91
107
  end
92
108
  end
93
109
 
110
+ def print
111
+ puts YAML.dump(@config.to_h)
112
+ end
113
+
94
114
  def check
95
115
  puts "Checking #{@config.root} ..."
96
116
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
@@ -10,6 +10,8 @@ module ThemeCheck
10
10
  attr_accessor :only_categories, :exclude_categories, :auto_correct
11
11
 
12
12
  class << self
13
+ attr_reader :last_loaded_config
14
+
13
15
  def from_path(path)
14
16
  if (filename = find(path))
15
17
  new(root: filename.dirname, configuration: load_file(filename))
@@ -36,6 +38,7 @@ module ThemeCheck
36
38
  end
37
39
 
38
40
  def load_file(absolute_path)
41
+ @last_loaded_config = absolute_path
39
42
  YAML.load_file(absolute_path)
40
43
  end
41
44
 
@@ -83,8 +86,8 @@ module ThemeCheck
83
86
 
84
87
  check_class = ThemeCheck.const_get(check_name)
85
88
 
86
- next if exclude_categories.include?(check_class.category)
87
- next if only_categories.any? && !only_categories.include?(check_class.category)
89
+ next if check_class.categories.any? { |category| exclude_categories.include?(category) }
90
+ next if only_categories.any? && check_class.categories.none? { |category| only_categories.include?(category) }
88
91
 
89
92
  options_for_check = options.transform_keys(&:to_sym)
90
93
  options_for_check.delete(:enabled)
@@ -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,24 +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 = {})
9
+ def initialize(files = {}, root = "/dev/null")
10
10
  @files = files
11
+ @root = Pathname.new(root)
11
12
  end
12
13
 
13
- def path(name)
14
- name
14
+ def path(relative_path)
15
+ @root.join(relative_path)
15
16
  end
16
17
 
17
- def read(name)
18
- @files[name]
18
+ def read(relative_path)
19
+ @files[relative_path]
19
20
  end
20
21
 
21
- def write(name, content)
22
- @files[name] = content
22
+ def write(relative_path, content)
23
+ @files[relative_path] = content
23
24
  end
24
25
 
25
26
  def files
26
- @values ||= @files.keys
27
+ @files.keys
27
28
  end
28
29
 
29
30
  def directories
@@ -33,5 +34,9 @@ module ThemeCheck
33
34
  .map(&:to_s)
34
35
  .uniq
35
36
  end
37
+
38
+ def relative_path(absolute_path)
39
+ Pathname.new(absolute_path).relative_path_from(@root).to_s
40
+ end
36
41
  end
37
42
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative "language_server/protocol"
3
+ require_relative "language_server/constants"
3
4
  require_relative "language_server/handler"
4
5
  require_relative "language_server/server"
5
6
  require_relative "language_server/tokens"
@@ -7,6 +8,7 @@ require_relative "language_server/position_helper"
7
8
  require_relative "language_server/completion_helper"
8
9
  require_relative "language_server/completion_provider"
9
10
  require_relative "language_server/completion_engine"
11
+ require_relative "language_server/document_link_engine"
10
12
 
11
13
  Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
12
14
  require file
@@ -7,11 +7,11 @@ module ThemeCheck
7
7
 
8
8
  def initialize(storage)
9
9
  @storage = storage
10
- @providers = CompletionProvider.all.map(&:new)
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?
@@ -20,16 +20,6 @@ module ThemeCheck
20
20
  def first_word(content)
21
21
  return content.match(WORD)[0] if content.match?(WORD)
22
22
  end
23
-
24
- def matches(s, re)
25
- start_at = 0
26
- matches = []
27
- while (m = s.match(re, start_at))
28
- matches.push(m)
29
- start_at = m.end(0)
30
- end
31
- matches
32
- end
33
23
  end
34
24
  end
35
25
  end
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  module LanguageServer
5
5
  class CompletionProvider
6
6
  include CompletionHelper
7
+ include RegexHelpers
7
8
 
8
9
  class << self
9
10
  def all
@@ -15,6 +16,10 @@ module ThemeCheck
15
16
  end
16
17
  end
17
18
 
19
+ def initialize(storage = InMemoryStorage.new)
20
+ @storage = storage
21
+ end
22
+
18
23
  def completions(content, cursor)
19
24
  raise NotImplementedError
20
25
  end
@@ -7,8 +7,8 @@ module ThemeCheck
7
7
 
8
8
  def completions(content, cursor)
9
9
  return [] unless can_complete?(content, cursor)
10
- ShopifyLiquid::Filter.labels
11
- .select { |w| w.starts_with?(partial(content, cursor)) }
10
+ available_labels
11
+ .select { |w| w.start_with?(partial(content, cursor)) }
12
12
  .map { |filter| filter_to_completion(filter) }
13
13
  end
14
14
 
@@ -21,6 +21,10 @@ module ThemeCheck
21
21
 
22
22
  private
23
23
 
24
+ def available_labels
25
+ @labels ||= ShopifyLiquid::Filter.labels - ShopifyLiquid::DeprecatedFilter.labels
26
+ end
27
+
24
28
  def cursor_on_filter?(content, cursor)
25
29
  return false unless content.match?(NAMED_FILTER)
26
30
  matches(content, NAMED_FILTER).any? do |match|
@@ -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
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class RenderSnippetCompletionProvider < CompletionProvider
6
+ def completions(content, cursor)
7
+ return [] unless cursor_on_quoted_argument?(content, cursor)
8
+ partial = snippet(content) || ''
9
+ snippets
10
+ .select { |x| x.start_with?(partial) }
11
+ .map { |x| snippet_to_completion(x) }
12
+ end
13
+
14
+ private
15
+
16
+ def cursor_on_quoted_argument?(content, cursor)
17
+ match = content.match(PARTIAL_RENDER)
18
+ return false if match.nil?
19
+ match.begin(:partial) <= cursor && cursor <= match.end(:partial)
20
+ end
21
+
22
+ def snippet(content)
23
+ match = content.match(PARTIAL_RENDER)
24
+ return if match.nil?
25
+ match[:partial]
26
+ end
27
+
28
+ def snippets
29
+ @storage
30
+ .files
31
+ .select { |x| x.include?('snippets/') }
32
+ end
33
+
34
+ def snippet_to_completion(file)
35
+ {
36
+ label: File.basename(file, '.liquid'),
37
+ kind: CompletionItemKinds::SNIPPET,
38
+ detail: file,
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end