theme-check 0.3.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -3
  3. data/CHANGELOG.md +61 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/Gemfile +5 -3
  6. data/README.md +11 -4
  7. data/RELEASING.md +2 -2
  8. data/config/default.yml +14 -0
  9. data/data/shopify_liquid/tags.yml +27 -0
  10. data/data/shopify_translation_keys.yml +850 -0
  11. data/docs/checks/CHECK_DOCS_TEMPLATE.md +47 -0
  12. data/docs/checks/asset_size_css.md +52 -0
  13. data/docs/checks/asset_size_javascript.md +79 -0
  14. data/docs/checks/convert_include_to_render.md +48 -0
  15. data/docs/checks/default_locale.md +46 -0
  16. data/docs/checks/deprecated_filter.md +46 -0
  17. data/docs/checks/img_width_and_height.md +79 -0
  18. data/docs/checks/liquid_tag.md +65 -0
  19. data/docs/checks/matching_schema_translations.md +93 -0
  20. data/docs/checks/matching_translations.md +72 -0
  21. data/docs/checks/missing_enable_comment.md +50 -0
  22. data/docs/checks/missing_required_template_files.md +26 -0
  23. data/docs/checks/missing_template.md +40 -0
  24. data/docs/checks/nested_snippet.md +69 -0
  25. data/docs/checks/parser_blocking_javascript.md +97 -0
  26. data/docs/checks/required_directories.md +25 -0
  27. data/docs/checks/required_layout_theme_object.md +28 -0
  28. data/docs/checks/space_inside_braces.md +63 -0
  29. data/docs/checks/syntax_error.md +49 -0
  30. data/docs/checks/template_length.md +50 -0
  31. data/docs/checks/translation_key_exists.md +63 -0
  32. data/docs/checks/undefined_object.md +53 -0
  33. data/docs/checks/unknown_filter.md +45 -0
  34. data/docs/checks/unused_assign.md +47 -0
  35. data/docs/checks/unused_snippet.md +32 -0
  36. data/docs/checks/valid_html_translation.md +53 -0
  37. data/docs/checks/valid_json.md +60 -0
  38. data/docs/checks/valid_schema.md +50 -0
  39. data/lib/theme_check.rb +3 -0
  40. data/lib/theme_check/asset_file.rb +34 -0
  41. data/lib/theme_check/check.rb +19 -9
  42. data/lib/theme_check/checks/asset_size_css.rb +89 -0
  43. data/lib/theme_check/checks/asset_size_javascript.rb +68 -0
  44. data/lib/theme_check/checks/convert_include_to_render.rb +1 -1
  45. data/lib/theme_check/checks/default_locale.rb +1 -0
  46. data/lib/theme_check/checks/deprecated_filter.rb +1 -1
  47. data/lib/theme_check/checks/img_width_and_height.rb +74 -0
  48. data/lib/theme_check/checks/liquid_tag.rb +3 -3
  49. data/lib/theme_check/checks/matching_schema_translations.rb +1 -0
  50. data/lib/theme_check/checks/matching_translations.rb +1 -0
  51. data/lib/theme_check/checks/missing_enable_comment.rb +1 -0
  52. data/lib/theme_check/checks/missing_required_template_files.rb +1 -2
  53. data/lib/theme_check/checks/missing_template.rb +1 -0
  54. data/lib/theme_check/checks/nested_snippet.rb +1 -0
  55. data/lib/theme_check/checks/parser_blocking_javascript.rb +7 -14
  56. data/lib/theme_check/checks/required_directories.rb +1 -1
  57. data/lib/theme_check/checks/required_layout_theme_object.rb +1 -1
  58. data/lib/theme_check/checks/space_inside_braces.rb +1 -0
  59. data/lib/theme_check/checks/syntax_error.rb +1 -0
  60. data/lib/theme_check/checks/template_length.rb +1 -0
  61. data/lib/theme_check/checks/translation_key_exists.rb +17 -1
  62. data/lib/theme_check/checks/undefined_object.rb +29 -10
  63. data/lib/theme_check/checks/unknown_filter.rb +1 -0
  64. data/lib/theme_check/checks/unused_assign.rb +5 -3
  65. data/lib/theme_check/checks/unused_snippet.rb +1 -0
  66. data/lib/theme_check/checks/valid_html_translation.rb +1 -0
  67. data/lib/theme_check/checks/valid_json.rb +1 -0
  68. data/lib/theme_check/checks/valid_schema.rb +1 -0
  69. data/lib/theme_check/cli.rb +39 -12
  70. data/lib/theme_check/config.rb +5 -2
  71. data/lib/theme_check/in_memory_storage.rb +11 -3
  72. data/lib/theme_check/language_server.rb +12 -0
  73. data/lib/theme_check/language_server/completion_engine.rb +38 -0
  74. data/lib/theme_check/language_server/completion_helper.rb +25 -0
  75. data/lib/theme_check/language_server/completion_provider.rb +28 -0
  76. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +51 -0
  77. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +31 -0
  78. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
  79. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +31 -0
  80. data/lib/theme_check/language_server/constants.rb +10 -0
  81. data/lib/theme_check/language_server/document_link_engine.rb +47 -0
  82. data/lib/theme_check/language_server/handler.rb +93 -6
  83. data/lib/theme_check/language_server/position_helper.rb +27 -0
  84. data/lib/theme_check/language_server/protocol.rb +41 -0
  85. data/lib/theme_check/language_server/server.rb +8 -2
  86. data/lib/theme_check/language_server/tokens.rb +55 -0
  87. data/lib/theme_check/liquid_check.rb +11 -0
  88. data/lib/theme_check/offense.rb +51 -14
  89. data/lib/theme_check/regex_helpers.rb +15 -0
  90. data/lib/theme_check/remote_asset_file.rb +44 -0
  91. data/lib/theme_check/shopify_liquid.rb +1 -0
  92. data/lib/theme_check/shopify_liquid/deprecated_filter.rb +4 -0
  93. data/lib/theme_check/shopify_liquid/tag.rb +16 -0
  94. data/lib/theme_check/theme.rb +7 -1
  95. data/lib/theme_check/version.rb +1 -1
  96. metadata +52 -2
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ PARTIAL_RENDER = %r{
6
+ \{\%\s*render\s+'(?<partial>[^']*)'|
7
+ \{\%\s*render\s+"(?<partial>[^"]*)"
8
+ }mix
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class DocumentLinkEngine
6
+ include PositionHelper
7
+ include RegexHelpers
8
+
9
+ def initialize(storage)
10
+ @storage = storage
11
+ end
12
+
13
+ def document_links(uri)
14
+ buffer = @storage.read(uri)
15
+ matches(buffer, PARTIAL_RENDER).map do |match|
16
+ start_line, start_character = from_index_to_line_column(
17
+ buffer,
18
+ match.begin(:partial),
19
+ )
20
+
21
+ end_line, end_character = from_index_to_line_column(
22
+ buffer,
23
+ match.end(:partial)
24
+ )
25
+
26
+ {
27
+ target: link(match[:partial]),
28
+ range: {
29
+ start: {
30
+ line: start_line,
31
+ character: start_character,
32
+ },
33
+ end: {
34
+ line: end_line,
35
+ character: end_character,
36
+ },
37
+ },
38
+ }
39
+ end
40
+ end
41
+
42
+ def link(partial)
43
+ 'file://' + @storage.path('snippets/' + partial + '.liquid')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,9 +4,14 @@ module ThemeCheck
4
4
  module LanguageServer
5
5
  class Handler
6
6
  CAPABILITIES = {
7
+ completionProvider: {
8
+ triggerCharacters: ['.', '{{ ', '{% '],
9
+ context: true,
10
+ },
11
+ documentLinkProvider: true,
7
12
  textDocumentSync: {
8
13
  openClose: true,
9
- change: false,
14
+ change: TextDocumentSyncKind::FULL,
10
15
  willSave: false,
11
16
  save: true,
12
17
  },
@@ -19,6 +24,9 @@ module ThemeCheck
19
24
 
20
25
  def on_initialize(id, params)
21
26
  @root_path = params["rootPath"]
27
+ @storage = in_memory_storage(@root_path)
28
+ @completion_engine = CompletionEngine.new(@storage)
29
+ @document_link_engine = DocumentLinkEngine.new(@storage)
22
30
  # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
23
31
  send_response(
24
32
  id: id,
@@ -31,14 +39,77 @@ module ThemeCheck
31
39
  def on_exit(_id, _params)
32
40
  close!
33
41
  end
42
+ alias_method :on_shutdown, :on_exit
43
+
44
+ def on_text_document_did_change(_id, params)
45
+ uri = text_document_uri(params)
46
+ @storage.write(uri, content_changes_text(params))
47
+ end
48
+
49
+ def on_text_document_did_close(_id, params)
50
+ uri = text_document_uri(params)
51
+ @storage.write(uri, nil)
52
+ end
34
53
 
35
54
  def on_text_document_did_open(_id, params)
36
- analyze_and_send_offenses(params.dig('textDocument', 'uri').sub('file://', ''))
55
+ uri = text_document_uri(params)
56
+ @storage.write(uri, text_document_text(params))
57
+ analyze_and_send_offenses(uri)
58
+ end
59
+
60
+ def on_text_document_did_save(_id, params)
61
+ analyze_and_send_offenses(text_document_uri(params))
62
+ end
63
+
64
+ def on_text_document_document_link(id, params)
65
+ uri = text_document_uri(params)
66
+ send_response(
67
+ id: id,
68
+ result: document_links(uri)
69
+ )
70
+ end
71
+
72
+ def on_text_document_completion(id, params)
73
+ uri = text_document_uri(params)
74
+ line = params.dig('position', 'line')
75
+ col = params.dig('position', 'character')
76
+ send_response(
77
+ id: id,
78
+ result: completions(uri, line, col)
79
+ )
37
80
  end
38
- alias_method :on_text_document_did_save, :on_text_document_did_open
39
81
 
40
82
  private
41
83
 
84
+ def in_memory_storage(root)
85
+ config = ThemeCheck::Config.from_path(root)
86
+
87
+ # Make a real FS to get the files from the snippets folder
88
+ fs = ThemeCheck::FileSystemStorage.new(
89
+ config.root,
90
+ ignored_patterns: config.ignored_patterns
91
+ )
92
+
93
+ # Turn that into a hash of empty buffers
94
+ files = fs.files
95
+ .map { |fn| [fn, ""] }
96
+ .to_h
97
+
98
+ InMemoryStorage.new(files, root)
99
+ end
100
+
101
+ def text_document_uri(params)
102
+ params.dig('textDocument', 'uri').sub('file://', '')
103
+ end
104
+
105
+ def text_document_text(params)
106
+ params.dig('textDocument', 'text')
107
+ end
108
+
109
+ def content_changes_text(params)
110
+ params.dig('contentChanges', 0, 'text')
111
+ end
112
+
42
113
  def analyze_and_send_offenses(file_path)
43
114
  root = ThemeCheck::Config.find(file_path) || @root_path
44
115
  config = ThemeCheck::Config.from_path(root)
@@ -60,6 +131,14 @@ module ThemeCheck
60
131
  analyzer.offenses
61
132
  end
62
133
 
134
+ def completions(uri, line, col)
135
+ @completion_engine.completions(uri, line, col)
136
+ end
137
+
138
+ def document_links(uri)
139
+ @document_link_engine.document_links(uri)
140
+ end
141
+
63
142
  def send_diagnostics(offenses)
64
143
  reported_files = Set.new
65
144
 
@@ -90,12 +169,20 @@ module ThemeCheck
90
169
  end
91
170
 
92
171
  def offense_to_diagnostic(offense)
93
- {
172
+ diagnostic = {
173
+ code: offense.code_name,
174
+ message: offense.message,
94
175
  range: range(offense),
95
176
  severity: severity(offense),
96
- code: offense.code_name,
97
177
  source: "theme-check",
98
- message: offense.message,
178
+ }
179
+ diagnostic["codeDescription"] = code_description(offense) unless offense.doc.nil?
180
+ diagnostic
181
+ end
182
+
183
+ def code_description(offense)
184
+ {
185
+ href: offense.doc,
99
186
  }
100
187
  end
101
188
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # Note: Everything is 0-indexed here.
3
+
4
+ module ThemeCheck
5
+ module LanguageServer
6
+ module PositionHelper
7
+ def from_line_column_to_index(content, row, col)
8
+ i = 0
9
+ result = 0
10
+ lines = content.lines
11
+ while i < row
12
+ result += lines[i].size
13
+ i += 1
14
+ end
15
+ result += col
16
+ result
17
+ end
18
+
19
+ def from_index_to_line_column(content, index)
20
+ lines = content[0..index].lines
21
+ row = lines.size - 1
22
+ col = lines.last.size - 1
23
+ [row, col]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # Here we define the Language Server Protocol Constants we're using.
3
+ # For complete docs, see the following:
4
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current
5
+ module ThemeCheck
6
+ module LanguageServer
7
+ module CompletionItemKinds
8
+ TEXT = 1
9
+ METHOD = 2
10
+ FUNCTION = 3
11
+ CONSTRUCTOR = 4
12
+ FIELD = 5
13
+ VARIABLE = 6
14
+ CLASS = 7
15
+ INTERFACE = 8
16
+ MODULE = 9
17
+ PROPERTY = 10
18
+ UNIT = 11
19
+ VALUE = 12
20
+ ENUM = 13
21
+ KEYWORD = 14
22
+ SNIPPET = 15
23
+ COLOR = 16
24
+ FILE = 17
25
+ REFERENCE = 18
26
+ FOLDER = 19
27
+ ENUM_MEMBER = 20
28
+ CONSTANT = 21
29
+ STRUCT = 22
30
+ EVENT = 23
31
+ OPERATOR = 24
32
+ TYPE_PARAMETER = 25
33
+ end
34
+
35
+ module TextDocumentSyncKind
36
+ NONE = 0
37
+ FULL = 1
38
+ INCREMENTAL = 2
39
+ end
40
+ end
41
+ end
@@ -6,15 +6,18 @@ require 'active_support/core_ext/string/inflections'
6
6
  module ThemeCheck
7
7
  module LanguageServer
8
8
  class DoneStreaming < StandardError; end
9
+
9
10
  class IncompatibleStream < StandardError; end
10
11
 
11
12
  class Server
12
13
  attr_reader :handler
14
+ attr_reader :should_raise_errors
13
15
 
14
16
  def initialize(
15
17
  in_stream: STDIN,
16
18
  out_stream: STDOUT,
17
- err_stream: $DEBUG ? File.open('/tmp/lsp.log', 'a') : STDERR
19
+ err_stream: STDERR,
20
+ should_raise_errors: false
18
21
  )
19
22
  validate!([in_stream, out_stream, err_stream])
20
23
 
@@ -25,6 +28,8 @@ module ThemeCheck
25
28
 
26
29
  @out.sync = true # do not buffer
27
30
  @err.sync = true # do not buffer
31
+
32
+ @should_raise_errors = should_raise_errors
28
33
  end
29
34
 
30
35
  def listen
@@ -37,6 +42,7 @@ module ThemeCheck
37
42
  return 0
38
43
 
39
44
  rescue Exception => e # rubocop:disable Lint/RescueException
45
+ raise e if should_raise_errors
40
46
  log(e)
41
47
  log(e.backtrace)
42
48
  return 1
@@ -45,7 +51,7 @@ module ThemeCheck
45
51
 
46
52
  def send_response(response)
47
53
  response_body = JSON.dump(response)
48
- log(response_body) if $DEBUG
54
+ log(JSON.pretty_generate(response)) if $DEBUG
49
55
 
50
56
  @out.write("Content-Length: #{response_body.size}\r\n")
51
57
  @out.write("\r\n")
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ Token = Struct.new(
5
+ :content,
6
+ :start, # inclusive
7
+ :end, # exclusive
8
+ )
9
+
10
+ TAG_START = Liquid::TagStart
11
+ TAG_END = Liquid::TagEnd
12
+ VARIABLE_START = Liquid::VariableStart
13
+ VARIABLE_END = Liquid::VariableEnd
14
+ SPLITTER = %r{
15
+ (?=(?:#{TAG_START}|#{VARIABLE_START}))| # positive lookahead on tag/variable start
16
+ (?<=(?:#{TAG_END}|#{VARIABLE_END})) # positive lookbehind on tag/variable end
17
+ }xom
18
+
19
+ # Implemented as an Enumerable so we stop iterating on the find once
20
+ # we have what we want. Kind of a perf thing.
21
+ class Tokens
22
+ include Enumerable
23
+
24
+ def initialize(buffer)
25
+ @buffer = buffer
26
+ end
27
+
28
+ def each(&block)
29
+ return to_enum(:each) unless block_given?
30
+
31
+ chunks = @buffer.split(SPLITTER)
32
+ chunks.shift if chunks[0]&.empty?
33
+
34
+ prev = Token.new('', 0, 0)
35
+ curr = Token.new('', 0, 0)
36
+
37
+ while (content = chunks.shift)
38
+
39
+ curr.start = prev.end
40
+ curr.end = curr.start + content.size
41
+
42
+ block.call(Token.new(
43
+ content,
44
+ curr.start,
45
+ curr.end,
46
+ ))
47
+
48
+ # recycling structs
49
+ tmp = prev
50
+ prev = curr
51
+ curr = tmp
52
+ end
53
+ end
54
+ end
55
+ end
@@ -6,6 +6,17 @@ module ThemeCheck
6
6
  extend ChecksTracking
7
7
  include ParsingHelpers
8
8
 
9
+ TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
10
+ VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
11
+ START_OR_END_QUOTE = /(^['"])|(['"]$)/
12
+ QUOTED_LIQUID_ATTRIBUTE = %r{
13
+ '(?:#{TAG}|#{VARIABLE}|[^'])*'| # any combination of tag/variable or non straight quote inside straight quotes
14
+ "(?:#{TAG}|#{VARIABLE}|[^"])*" # any combination of tag/variable or non double quotes inside double quotes
15
+ }omix
16
+ ATTR = /[a-z0-9-]+/i
17
+ HTML_ATTRIBUTE = /#{ATTR}(?:=#{QUOTED_LIQUID_ATTRIBUTE})?/omix
18
+ HTML_ATTRIBUTES = /(?:#{HTML_ATTRIBUTE}|\s)*/omix
19
+
9
20
  def add_offense(message, node: nil, template: node&.template, markup: nil, line_number: nil, &block)
10
21
  offenses << Offense.new(check: self, message: message, template: template, node: node, markup: markup, line_number: line_number, correction: block)
11
22
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
+ Position = Struct.new(:line, :column)
4
+
3
5
  class Offense
4
6
  MAX_SOURCE_EXCERPT_SIZE = 120
5
7
 
@@ -35,6 +37,9 @@ module ThemeCheck
35
37
  elsif @node
36
38
  @node.line_number
37
39
  end
40
+
41
+ @start_position = nil
42
+ @end_position = nil
38
43
  end
39
44
 
40
45
  def source_excerpt
@@ -50,27 +55,19 @@ module ThemeCheck
50
55
  end
51
56
 
52
57
  def start_line
53
- return 0 unless line_number
54
- line_number - 1
58
+ start_position.line
55
59
  end
56
60
 
57
- def end_line
58
- if markup
59
- start_line + markup.count("\n")
60
- else
61
- start_line
62
- end
61
+ def start_column
62
+ start_position.column
63
63
  end
64
64
 
65
- def start_column
66
- return 0 unless line_number && markup
67
- template.full_line(start_line + 1).index(markup.split("\n", 2).first)
65
+ def end_line
66
+ end_position.line
68
67
  end
69
68
 
70
69
  def end_column
71
- return 0 unless line_number && markup
72
- markup_end = markup.split("\n").last
73
- template.full_line(end_line + 1).index(markup_end) + markup_end.size
70
+ end_position.column
74
71
  end
75
72
 
76
73
  def code_name
@@ -116,5 +113,45 @@ module ThemeCheck
116
113
  message
117
114
  end
118
115
  end
116
+
117
+ private
118
+
119
+ def full_line(line)
120
+ # Liquid::Template is 1-indexed.
121
+ template.full_line(line + 1)
122
+ end
123
+
124
+ def lines_of_content
125
+ @lines ||= markup.lines.map { |x| x.sub(/\n$/, '') }
126
+ end
127
+
128
+ # 0-indexed, inclusive
129
+ def start_position
130
+ return @start_position if @start_position
131
+ return @start_position = Position.new(0, 0) unless line_number && markup
132
+
133
+ position = Position.new
134
+ position.line = line_number - 1
135
+ position.column = full_line(position.line).index(lines_of_content.first) || 0
136
+
137
+ @start_position = position
138
+ end
139
+
140
+ # 0-indexed, exclusive. It's the line + col that are exclusive.
141
+ # This is why it doesn't make sense to calculate them separately.
142
+ def end_position
143
+ return @end_position if @end_position
144
+ return @end_position = Position.new(0, 0) unless line_number && markup
145
+
146
+ position = Position.new
147
+ position.line = start_line + lines_of_content.size - 1
148
+ position.column = if start_line == position.line
149
+ start_column + markup.size
150
+ else
151
+ lines_of_content.last.size
152
+ end
153
+
154
+ @end_position = position
155
+ end
119
156
  end
120
157
  end