theme-check 0.6.0 → 0.8.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/theme-check.yml +11 -3
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +29 -0
  5. data/Gemfile +5 -5
  6. data/LICENSE.md +2 -0
  7. data/README.md +1 -0
  8. data/RELEASING.md +10 -3
  9. data/Rakefile +6 -0
  10. data/config/default.yml +3 -0
  11. data/dev.yml +1 -1
  12. data/docs/checks/remote_asset.md +82 -0
  13. data/exe/theme-check +1 -1
  14. data/lib/theme_check.rb +1 -0
  15. data/lib/theme_check/check.rb +1 -1
  16. data/lib/theme_check/checks/asset_size_css.rb +1 -1
  17. data/lib/theme_check/checks/asset_size_javascript.rb +1 -1
  18. data/lib/theme_check/checks/img_width_and_height.rb +2 -2
  19. data/lib/theme_check/checks/matching_translations.rb +1 -1
  20. data/lib/theme_check/checks/parser_blocking_javascript.rb +1 -1
  21. data/lib/theme_check/checks/remote_asset.rb +99 -0
  22. data/lib/theme_check/checks/translation_key_exists.rb +1 -4
  23. data/lib/theme_check/checks/undefined_object.rb +1 -1
  24. data/lib/theme_check/checks/valid_html_translation.rb +1 -1
  25. data/lib/theme_check/cli.rb +101 -57
  26. data/lib/theme_check/config.rb +6 -2
  27. data/lib/theme_check/disabled_checks.rb +2 -2
  28. data/lib/theme_check/in_memory_storage.rb +13 -16
  29. data/lib/theme_check/language_server/completion_engine.rb +2 -2
  30. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +1 -1
  31. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
  32. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +1 -1
  33. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +2 -2
  34. data/lib/theme_check/language_server/constants.rb +2 -2
  35. data/lib/theme_check/language_server/document_link_engine.rb +4 -3
  36. data/lib/theme_check/language_server/handler.rb +29 -21
  37. data/lib/theme_check/language_server/server.rb +1 -2
  38. data/lib/theme_check/node.rb +1 -2
  39. data/lib/theme_check/offense.rb +3 -1
  40. data/lib/theme_check/packager.rb +1 -1
  41. data/lib/theme_check/parsing_helpers.rb +1 -1
  42. data/lib/theme_check/releaser.rb +39 -0
  43. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +6 -8
  44. data/lib/theme_check/shopify_liquid/filter.rb +3 -5
  45. data/lib/theme_check/shopify_liquid/object.rb +2 -6
  46. data/lib/theme_check/shopify_liquid/tag.rb +1 -3
  47. data/lib/theme_check/storage.rb +3 -3
  48. data/lib/theme_check/string_helpers.rb +47 -0
  49. data/lib/theme_check/tags.rb +1 -2
  50. data/lib/theme_check/theme.rb +1 -1
  51. data/lib/theme_check/version.rb +1 -1
  52. data/packaging/homebrew/theme_check.base.rb +1 -1
  53. data/theme-check.gemspec +3 -2
  54. metadata +9 -19
@@ -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)
@@ -1,80 +1,104 @@
1
1
  # frozen_string_literal: true
2
+ require "optparse"
3
+
2
4
  module ThemeCheck
3
5
  class Cli
4
6
  class Abort < StandardError; end
5
7
 
6
- USAGE = <<~END
7
- Usage: theme-check [options] /path/to/your/theme
8
+ attr_accessor :path
8
9
 
9
- Options:
10
- --init Generate a .theme-check.yml file in the current directory
11
- -C, --config <path> Use the config provided, overriding .theme-check.yml if present
12
- -c, --category <category> Only run this category of checks
13
- -x, --exclude-category <category> Exclude this category of checks
14
- -l, --list List enabled checks
15
- -a, --auto-correct Automatically fix offenses
16
- -h, --help Show this. Hi!
17
- -v, --version Print Theme Check version
10
+ def initialize
11
+ @path = "."
12
+ @command = :check
13
+ @only_categories = []
14
+ @exclude_categories = []
15
+ @auto_correct = false
16
+ @config_path = nil
17
+ end
18
18
 
19
- Description:
20
- Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
21
- Liquid & JSON inside your theme.
19
+ def option_parser(parser = OptionParser.new, help: true)
20
+ return @option_parser if defined?(@option_parser)
21
+ @option_parser = parser
22
+ @option_parser.banner = "Usage: theme-check [options] [/path/to/your/theme]"
22
23
 
23
- You can configure checks in the .theme-check.yml file of your theme root directory.
24
- END
24
+ @option_parser.separator("")
25
+ @option_parser.separator("Basic Options:")
26
+ @option_parser.on(
27
+ "-C", "--config PATH",
28
+ "Use the config provided, overriding .theme-check.yml if present"
29
+ ) { |path| @config_path = path }
30
+ @option_parser.on(
31
+ "-c", "--category CATEGORY",
32
+ "Only run this category of checks"
33
+ ) { |category| @only_categories << category.to_sym }
34
+ @option_parser.on(
35
+ "-x", "--exclude-category CATEGORY",
36
+ "Exclude this category of checks"
37
+ ) { |category| @exclude_categories << category.to_sym }
38
+ @option_parser.on(
39
+ "-a", "--auto-correct",
40
+ "Automatically fix offenses"
41
+ ) { @auto_correct = true }
25
42
 
26
- def run(argv)
27
- @path = "."
43
+ @option_parser.separator("")
44
+ @option_parser.separator("Miscellaneous:")
45
+ @option_parser.on(
46
+ "--init",
47
+ "Generate a .theme-check.yml file"
48
+ ) { @command = :init }
49
+ @option_parser.on(
50
+ "--print",
51
+ "Output active config to STDOUT"
52
+ ) { @command = :print }
53
+ @option_parser.on(
54
+ "-h", "--help",
55
+ "Show this. Hi!"
56
+ ) { @command = :help } if help
57
+ @option_parser.on(
58
+ "-l", "--list",
59
+ "List enabled checks"
60
+ ) { @command = :list }
61
+ @option_parser.on(
62
+ "-v", "--version",
63
+ "Print Theme Check version"
64
+ ) { @command = :version }
28
65
 
29
- command = :check
30
- only_categories = []
31
- exclude_categories = []
32
- auto_correct = false
33
- config_path = nil
34
-
35
- args = argv.dup
36
- while (arg = args.shift)
37
- case arg
38
- when "--help", "-h"
39
- raise Abort, USAGE
40
- when "--version", "-v"
41
- command = :version
42
- when "--config", "-C"
43
- config_path = Pathname.new(args.shift)
44
- when "--category", "-c"
45
- only_categories << args.shift.to_sym
46
- when "--exclude-category", "-x"
47
- exclude_categories << args.shift.to_sym
48
- when "--list", "-l"
49
- command = :list
50
- when "--auto-correct", "-a"
51
- auto_correct = true
52
- when "--init"
53
- command = :init
54
- else
55
- @path = arg
56
- end
57
- end
66
+ @option_parser.separator("")
67
+ @option_parser.separator(<<~EOS)
68
+ Description:
69
+ Theme Check helps you follow Shopify Themes & Liquid best practices by analyzing the
70
+ Liquid & JSON inside your theme.
71
+
72
+ You can configure checks in the .theme-check.yml file of your theme root directory.
73
+ EOS
74
+
75
+ @option_parser
76
+ end
58
77
 
59
- unless [:version, :init].include?(command)
60
- @config = if config_path.present?
78
+ def parse(argv)
79
+ @path = option_parser.parse(argv).first || "."
80
+ end
81
+
82
+ def run!
83
+ unless [:version, :init, :help].include?(@command)
84
+ @config = if @config_path
61
85
  ThemeCheck::Config.new(
62
86
  root: @path,
63
- configuration: ThemeCheck::Config.load_file(config_path)
87
+ configuration: ThemeCheck::Config.load_file(@config_path)
64
88
  )
65
89
  else
66
90
  ThemeCheck::Config.from_path(@path)
67
91
  end
68
- @config.only_categories = only_categories
69
- @config.exclude_categories = exclude_categories
70
- @config.auto_correct = auto_correct
92
+ @config.only_categories = @only_categories
93
+ @config.exclude_categories = @exclude_categories
94
+ @config.auto_correct = @auto_correct
71
95
  end
72
96
 
73
- send(command)
97
+ send(@command)
74
98
  end
75
99
 
76
- def run!(argv)
77
- run(argv)
100
+ def run
101
+ run!
78
102
  rescue Abort => e
79
103
  if e.message.empty?
80
104
  exit(1)
@@ -83,6 +107,18 @@ module ThemeCheck
83
107
  end
84
108
  end
85
109
 
110
+ def self.parse_and_run!(argv)
111
+ cli = new
112
+ cli.parse(argv)
113
+ cli.run!
114
+ end
115
+
116
+ def self.parse_and_run(argv)
117
+ cli = new
118
+ cli.parse(argv)
119
+ cli.run
120
+ end
121
+
86
122
  def list
87
123
  puts @config.enabled_checks
88
124
  end
@@ -102,12 +138,20 @@ module ThemeCheck
102
138
  end
103
139
  end
104
140
 
141
+ def print
142
+ puts YAML.dump(@config.to_h)
143
+ end
144
+
145
+ def help
146
+ puts option_parser.to_s
147
+ end
148
+
105
149
  def check
106
150
  puts "Checking #{@config.root} ..."
107
151
  storage = ThemeCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
108
152
  theme = ThemeCheck::Theme.new(storage)
109
153
  if theme.all.empty?
110
- raise Abort, "No templates found.\n#{USAGE}"
154
+ raise Abort, "No templates found."
111
155
  end
112
156
  analyzer = ThemeCheck::Analyzer.new(theme, @config.enabled_checks, @config.auto_correct)
113
157
  analyzer.analyze_theme
@@ -91,7 +91,11 @@ module ThemeCheck
91
91
 
92
92
  options_for_check = options.transform_keys(&:to_sym)
93
93
  options_for_check.delete(:enabled)
94
- check = check_class.new(**options_for_check)
94
+ check = if options_for_check.empty?
95
+ check_class.new
96
+ else
97
+ check_class.new(**options_for_check)
98
+ end
95
99
  check.options = options_for_check
96
100
  check
97
101
  end.compact
@@ -104,7 +108,7 @@ module ThemeCheck
104
108
  private
105
109
 
106
110
  def check_name?(name)
107
- name.start_with?(/[A-Z]/)
111
+ name.to_s.start_with?(/[A-Z]/)
108
112
  end
109
113
 
110
114
  def validate_configuration(configuration, default_configuration = self.class.default, parent_keys = [])
@@ -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
  )
@@ -3,8 +3,8 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  PARTIAL_RENDER = %r{
6
- \{\%\s*render\s+'(?<partial>[^']*)'|
7
- \{\%\s*render\s+"(?<partial>[^"]*)"
6
+ \{\%-?\s*render\s+'(?<partial>[^']*)'|
7
+ \{\%-?\s*render\s+"(?<partial>[^"]*)"
8
8
  }mix
9
9
  end
10
10
  end
@@ -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
  )