theme-check 0.3.0 → 0.5.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/CHANGELOG.md +50 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/README.md +9 -4
  6. data/RELEASING.md +2 -2
  7. data/config/default.yml +7 -0
  8. data/data/shopify_liquid/tags.yml +27 -0
  9. data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
  10. data/docs/checks/asset_size_javascript.md +79 -0
  11. data/docs/checks/convert_include_to_render.md +48 -0
  12. data/docs/checks/default_locale.md +46 -0
  13. data/docs/checks/deprecated_filter.md +46 -0
  14. data/docs/checks/liquid_tag.md +65 -0
  15. data/docs/checks/matching_schema_translations.md +93 -0
  16. data/docs/checks/matching_translations.md +72 -0
  17. data/docs/checks/missing_enable_comment.md +50 -0
  18. data/docs/checks/missing_required_template_files.md +26 -0
  19. data/docs/checks/missing_template.md +40 -0
  20. data/docs/checks/nested_snippet.md +69 -0
  21. data/docs/checks/parser_blocking_javascript.md +97 -0
  22. data/docs/checks/required_directories.md +25 -0
  23. data/docs/checks/required_layout_theme_object.md +28 -0
  24. data/docs/checks/space_inside_braces.md +63 -0
  25. data/docs/checks/syntax_error.md +49 -0
  26. data/docs/checks/template_length.md +50 -0
  27. data/docs/checks/translation_key_exists.md +63 -0
  28. data/docs/checks/undefined_object.md +53 -0
  29. data/docs/checks/unknown_filter.md +45 -0
  30. data/docs/checks/unused_assign.md +47 -0
  31. data/docs/checks/unused_snippet.md +32 -0
  32. data/docs/checks/valid_html_translation.md +53 -0
  33. data/docs/checks/valid_json.md +60 -0
  34. data/docs/checks/valid_schema.md +50 -0
  35. data/lib/theme_check.rb +4 -0
  36. data/lib/theme_check/asset_file.rb +34 -0
  37. data/lib/theme_check/check.rb +19 -9
  38. data/lib/theme_check/checks/asset_size_javascript.rb +74 -0
  39. data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
  40. data/lib/theme_check/checks/default_locale.rb +1 -0
  41. data/lib/theme_check/checks/deprecated_filter.rb +1 -1
  42. data/lib/theme_check/checks/liquid_tag.rb +3 -3
  43. data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
  44. data/lib/theme_check/checks/matching_translations.rb +1 -0
  45. data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
  46. data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
  47. data/lib/theme_check/checks/missing_template.rb +1 -0
  48. data/lib/theme_check/checks/nested_snippet.rb +1 -0
  49. data/lib/theme_check/checks/parser_blocking_javascript.rb +2 -1
  50. data/lib/theme_check/checks/required_directories.rb +1 -1
  51. data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
  52. data/lib/theme_check/checks/space_inside_braces.rb +1 -0
  53. data/lib/theme_check/checks/syntax_error.rb +1 -0
  54. data/lib/theme_check/checks/template_length.rb +1 -0
  55. data/lib/theme_check/checks/translation_key_exists.rb +1 -0
  56. data/lib/theme_check/checks/undefined_object.rb +29 -10
  57. data/lib/theme_check/checks/unknown_filter.rb +1 -0
  58. data/lib/theme_check/checks/unused_assign.rb +5 -3
  59. data/lib/theme_check/checks/unused_snippet.rb +1 -0
  60. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  61. data/lib/theme_check/checks/valid_json.rb +1 -0
  62. data/lib/theme_check/checks/valid_schema.rb +1 -0
  63. data/lib/theme_check/cli.rb +22 -6
  64. data/lib/theme_check/config.rb +2 -2
  65. data/lib/theme_check/in_memory_storage.rb +1 -1
  66. data/lib/theme_check/language_server.rb +10 -0
  67. data/lib/theme_check/language_server/completion_engine.rb +38 -0
  68. data/lib/theme_check/language_server/completion_helper.rb +25 -0
  69. data/lib/theme_check/language_server/completion_provider.rb +24 -0
  70. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +47 -0
  71. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
  72. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
  73. data/lib/theme_check/language_server/handler.rb +62 -6
  74. data/lib/theme_check/language_server/position_helper.rb +27 -0
  75. data/lib/theme_check/language_server/protocol.rb +41 -0
  76. data/lib/theme_check/language_server/server.rb +6 -1
  77. data/lib/theme_check/language_server/tokens.rb +55 -0
  78. data/lib/theme_check/offense.rb +51 -14
  79. data/lib/theme_check/regex_helpers.rb +15 -0
  80. data/lib/theme_check/remote_asset_file.rb +44 -0
  81. data/lib/theme_check/shopify_liquid.rb +1 -0
  82. data/lib/theme_check/shopify_liquid/tag.rb +16 -0
  83. data/lib/theme_check/theme.rb +7 -1
  84. data/lib/theme_check/version.rb +1 -1
  85. metadata +44 -2
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class DefaultLocale < JsonCheck
4
4
  severity :suggestion
5
5
  category :translation
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def on_end
8
9
  return if @theme.default_locale_json
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
3
  class DeprecatedFilter < LiquidCheck
4
- doc "https://shopify.dev/docs/themes/liquid/reference/filters/deprecated-filters"
4
+ doc docs_url(__FILE__)
5
5
  category :liquid
6
6
  severity :suggestion
7
7
 
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- # Recommends using {% liquid ... %} if 3 or more consecutive {% ... %} are found.
3
+ # Recommends using {% liquid ... %} if 4 or more consecutive {% ... %} are found.
4
4
  class LiquidTag < LiquidCheck
5
5
  severity :suggestion
6
6
  category :liquid
7
- doc "https://shopify.dev/docs/themes/liquid/reference/tags/theme-tags#liquid"
7
+ doc docs_url(__FILE__)
8
8
 
9
- def initialize(min_consecutive_statements: 10)
9
+ def initialize(min_consecutive_statements: 4)
10
10
  @first_statement = nil
11
11
  @consecutive_statements = 0
12
12
  @min_consecutive_statements = min_consecutive_statements
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class MatchingSchemaTranslations < LiquidCheck
4
4
  severity :suggestion
5
5
  category :translation
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def on_schema(node)
8
9
  schema = JSON.parse(node.value.nodelist.join)
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class MatchingTranslations < JsonCheck
5
5
  severity :suggestion
6
6
  category :translation
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  def initialize
9
10
  @files = []
@@ -2,6 +2,7 @@
2
2
  module ThemeCheck
3
3
  class MissingEnableComment < LiquidCheck
4
4
  severity :error
5
+ doc docs_url(__FILE__)
5
6
 
6
7
  # Don't allow this check to be disabled with a comment,
7
8
  # as we need to be able to check for disabled checks.
@@ -3,11 +3,10 @@
3
3
  module ThemeCheck
4
4
  # Reports missing shopify required theme files
5
5
  # required templates: https://shopify.dev/tutorials/review-theme-store-requirements-files
6
-
7
6
  class MissingRequiredTemplateFiles < LiquidCheck
8
7
  severity :error
9
8
  category :liquid
10
- doc "https://shopify.dev/docs/themes/theme-templates"
9
+ doc docs_url(__FILE__)
11
10
 
12
11
  REQUIRED_LIQUID_FILES = %w(layout/theme)
13
12
  REQUIRED_TEMPLATE_FILES = %w(
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class MissingTemplate < LiquidCheck
5
5
  severity :suggestion
6
6
  category :liquid
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  def on_include(node)
9
10
  template = node.value.template_name_expr
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class NestedSnippet < LiquidCheck
5
5
  severity :suggestion
6
6
  category :liquid
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  class TemplateInfo < Struct.new(:includes)
9
10
  def with_deep_nested(templates, max, current_level = 0)
@@ -3,7 +3,8 @@ module ThemeCheck
3
3
  # Reports errors when trying to use parser-blocking script tags
4
4
  class ParserBlockingJavaScript < LiquidCheck
5
5
  severity :error
6
- category :liquid
6
+ categories :liquid, :performance
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  PARSER_BLOCKING_SCRIPT_TAG = %r{
9
10
  <script # Find the start of a script tag
@@ -5,7 +5,7 @@ module ThemeCheck
5
5
  class RequiredDirectories < LiquidCheck
6
6
  severity :error
7
7
  category :liquid
8
- doc "https://shopify.dev/tutorials/develop-theme-files"
8
+ doc docs_url(__FILE__)
9
9
 
10
10
  REQUIRED_DIRECTORIES = %w(assets config layout locales sections snippets templates)
11
11
 
@@ -4,7 +4,7 @@ module ThemeCheck
4
4
  class RequiredLayoutThemeObject < LiquidCheck
5
5
  severity :error
6
6
  category :liquid
7
- doc "https://shopify.dev/docs/themes/theme-templates/theme-liquid"
7
+ doc docs_url(__FILE__)
8
8
 
9
9
  LAYOUT_FILENAME = "layout/theme"
10
10
 
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class SpaceInsideBraces < LiquidCheck
5
5
  severity :style
6
6
  category :liquid
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  def initialize
9
10
  @ignore = false
@@ -4,6 +4,7 @@ module ThemeCheck
4
4
  class SyntaxError < LiquidCheck
5
5
  severity :error
6
6
  category :liquid
7
+ doc docs_url(__FILE__)
7
8
 
8
9
  def on_document(node)
9
10
  node.template.warnings.each do |warning|
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class TemplateLength < LiquidCheck
4
4
  severity :suggestion
5
5
  category :liquid
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def initialize(max_length: 200, exclude_schema: true)
8
9
  @max_length = max_length
@@ -3,6 +3,7 @@ module ThemeCheck
3
3
  class TranslationKeyExists < LiquidCheck
4
4
  severity :error
5
5
  category :translation
6
+ doc docs_url(__FILE__)
6
7
 
7
8
  def on_variable(node)
8
9
  return unless @theme.default_locale_json&.content&.is_a?(Hash)
@@ -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
@@ -49,23 +55,28 @@ module ThemeCheck
49
55
  end
50
56
  end
51
57
 
52
- def initialize
58
+ def initialize(exclude_snippets: false)
59
+ @exclude_snippets = exclude_snippets
53
60
  @files = {}
54
61
  end
55
62
 
56
63
  def on_document(node)
64
+ return if ignore?(node)
57
65
  @files[node.template.name] = TemplateInfo.new
58
66
  end
59
67
 
60
68
  def on_assign(node)
69
+ return if ignore?(node)
61
70
  @files[node.template.name].all_assigns[node.value.to] = node
62
71
  end
63
72
 
64
73
  def on_capture(node)
74
+ return if ignore?(node)
65
75
  @files[node.template.name].all_captures[node.value.instance_variable_get('@to')] = node
66
76
  end
67
77
 
68
78
  def on_for(node)
79
+ return if ignore?(node)
69
80
  @files[node.template.name].all_forloops[node.value.variable_name] = node
70
81
  end
71
82
 
@@ -75,6 +86,7 @@ module ThemeCheck
75
86
  end
76
87
 
77
88
  def on_render(node)
89
+ return if ignore?(node)
78
90
  return unless node.value.template_name_expr.is_a?(String)
79
91
 
80
92
  snippet_name = "snippets/#{node.value.template_name_expr}"
@@ -85,6 +97,7 @@ module ThemeCheck
85
97
  end
86
98
 
87
99
  def on_variable_lookup(node)
100
+ return if ignore?(node)
88
101
  @files[node.template.name].add_variable_lookup(
89
102
  name: node.value.name,
90
103
  node: node,
@@ -114,15 +127,20 @@ module ThemeCheck
114
127
  end
115
128
  end
116
129
 
130
+ private
131
+
132
+ def ignore?(node)
133
+ @exclude_snippets && node.template.snippet?
134
+ end
135
+
117
136
  def each_template
118
137
  @files.each do |(name, info)|
119
138
  next if name.starts_with?('snippets/')
120
139
  yield [name, info]
121
140
  end
122
141
  end
123
- private :each_template
124
142
 
125
- def check_object(info, all_global_objects, render_node = nil)
143
+ def check_object(info, all_global_objects, render_node = nil, visited_snippets = Set.new)
126
144
  check_undefined(info, all_global_objects, render_node)
127
145
 
128
146
  info.each_snippet do |(snippet_name, node)|
@@ -131,16 +149,18 @@ module ThemeCheck
131
149
 
132
150
  snippet_variables = node.value.attributes.keys +
133
151
  Array[node.value.instance_variable_get("@alias_name")]
134
- check_object(snippet_info, all_global_objects + snippet_variables, node)
152
+ unless visited_snippets.include?(snippet_name)
153
+ visited_snippets << snippet_name
154
+ check_object(snippet_info, all_global_objects + snippet_variables, node, visited_snippets)
155
+ end
135
156
  end
136
157
  end
137
- private :check_object
138
158
 
139
159
  def check_undefined(info, all_global_objects, render_node)
140
160
  all_variables = info.all_variables
141
161
 
142
162
  info.each_variable_lookup(!!render_node) do |(key, node)|
143
- name, _line_number = key
163
+ name, line_number = key
144
164
  next if all_variables.include?(name)
145
165
  next if all_global_objects.include?(name)
146
166
 
@@ -150,10 +170,9 @@ module ThemeCheck
150
170
  if render_node
151
171
  add_offense("Missing argument `#{name}`", node: render_node)
152
172
  else
153
- add_offense("Undefined object `#{name}`", node: node)
173
+ add_offense("Undefined object `#{name}`", node: node, line_number: line_number)
154
174
  end
155
175
  end
156
176
  end
157
- private :check_undefined
158
177
  end
159
178
  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,14 +4,16 @@ 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
- def collect_used_assigns(templates)
10
+ def collect_used_assigns(templates, visited = Set.new)
10
11
  collected = used_assigns
11
12
  # Check recursively inside included snippets for use
12
13
  includes.each do |name|
13
- if templates[name]
14
- collected += templates[name].collect_used_assigns(templates)
14
+ if templates[name] && !visited.include?(name)
15
+ visited << name
16
+ collected += templates[name].collect_used_assigns(templates, visited)
15
17
  end
16
18
  end
17
19
  collected
@@ -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,6 +5,7 @@ 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
11
  return unless file.name.starts_with?("locales/")
@@ -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)
@@ -11,6 +11,7 @@ module ThemeCheck
11
11
  -x, [--exclude-category] # Exclude this category of checks
12
12
  -l, [--list] # List enabled checks
13
13
  -a, [--auto-correct] # Automatically fix offenses
14
+ --init # Generate a .theme-check.yml file in the current directory
14
15
  -h, [--help] # Show this. Hi!
15
16
  -v, [--version] # Print Theme Check version
16
17
 
@@ -22,7 +23,7 @@ module ThemeCheck
22
23
  END
23
24
 
24
25
  def run(argv)
25
- path = "."
26
+ @path = "."
26
27
 
27
28
  command = :check
28
29
  only_categories = []
@@ -44,15 +45,19 @@ module ThemeCheck
44
45
  command = :list
45
46
  when "--auto-correct", "-a"
46
47
  auto_correct = true
48
+ when "--init"
49
+ command = :init
47
50
  else
48
- path = arg
51
+ @path = arg
49
52
  end
50
53
  end
51
54
 
52
- @config = ThemeCheck::Config.from_path(path)
53
- @config.only_categories = only_categories
54
- @config.exclude_categories = exclude_categories
55
- @config.auto_correct = auto_correct
55
+ unless [:version, :init].include?(command)
56
+ @config = ThemeCheck::Config.from_path(@path)
57
+ @config.only_categories = only_categories
58
+ @config.exclude_categories = exclude_categories
59
+ @config.auto_correct = auto_correct
60
+ end
56
61
 
57
62
  send(command)
58
63
  end
@@ -75,6 +80,17 @@ module ThemeCheck
75
80
  puts ThemeCheck::VERSION
76
81
  end
77
82
 
83
+ def init
84
+ dotfile_path = ThemeCheck::Config.find(@path)
85
+ if dotfile_path.nil?
86
+ File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config::DEFAULT_CONFIG))
87
+
88
+ puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
89
+ else
90
+ raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}."
91
+ end
92
+ end
93
+
78
94
  def check
79
95
  puts "Checking #{@config.root} ..."
80
96
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
@@ -83,8 +83,8 @@ module ThemeCheck
83
83
 
84
84
  check_class = ThemeCheck.const_get(check_name)
85
85
 
86
- next if exclude_categories.include?(check_class.category)
87
- next if only_categories.any? && !only_categories.include?(check_class.category)
86
+ next if check_class.categories.any? { |category| exclude_categories.include?(category) }
87
+ next if only_categories.any? && check_class.categories.none? { |category| only_categories.include?(category) }
88
88
 
89
89
  options_for_check = options.transform_keys(&:to_sym)
90
90
  options_for_check.delete(:enabled)
@@ -6,7 +6,7 @@
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 = {})
10
10
  @files = files
11
11
  end
12
12
 
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "language_server/protocol"
2
3
  require_relative "language_server/handler"
3
4
  require_relative "language_server/server"
5
+ require_relative "language_server/tokens"
6
+ require_relative "language_server/position_helper"
7
+ require_relative "language_server/completion_helper"
8
+ require_relative "language_server/completion_provider"
9
+ require_relative "language_server/completion_engine"
10
+
11
+ Dir[__dir__ + "/language_server/completion_providers/*.rb"].each do |file|
12
+ require file
13
+ end
4
14
 
5
15
  module ThemeCheck
6
16
  module LanguageServer