theme-check 1.11.0 → 1.12.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +82 -0
  5. data/README.md +4 -0
  6. data/Rakefile +7 -0
  7. data/TROUBLESHOOTING.md +65 -0
  8. data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
  9. data/data/shopify_liquid/documentation/filters.json +5528 -0
  10. data/data/shopify_liquid/documentation/latest.json +1 -0
  11. data/data/shopify_liquid/documentation/objects.json +19272 -0
  12. data/data/shopify_liquid/documentation/tags.json +1252 -0
  13. data/lib/theme_check/checks/undefined_object.rb +4 -0
  14. data/lib/theme_check/language_server/completion_context.rb +52 -0
  15. data/lib/theme_check/language_server/completion_engine.rb +15 -21
  16. data/lib/theme_check/language_server/completion_provider.rb +16 -1
  17. data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
  18. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
  19. data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
  20. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
  21. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
  22. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
  23. data/lib/theme_check/language_server/handler.rb +3 -1
  24. data/lib/theme_check/language_server/protocol.rb +9 -0
  25. data/lib/theme_check/language_server/type_helper.rb +22 -0
  26. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
  27. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
  28. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
  29. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  30. data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
  31. data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  32. data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  33. data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
  34. data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
  35. data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
  36. data/lib/theme_check/language_server.rb +12 -0
  37. data/lib/theme_check/remote_asset_file.rb +13 -7
  38. data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
  39. data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
  40. data/lib/theme_check/shopify_liquid/filter.rb +4 -0
  41. data/lib/theme_check/shopify_liquid/object.rb +4 -0
  42. data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
  43. data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
  44. data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
  45. data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
  46. data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
  47. data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
  48. data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
  49. data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
  50. data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
  51. data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
  52. data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
  53. data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
  54. data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
  55. data/lib/theme_check/shopify_liquid/tag.rb +4 -0
  56. data/lib/theme_check/shopify_liquid.rb +17 -1
  57. data/lib/theme_check/version.rb +1 -1
  58. data/shipit.rubygems.yml +3 -0
  59. data/theme-check.gemspec +3 -1
  60. metadata +37 -2
@@ -120,6 +120,10 @@ module ThemeCheck
120
120
  # NOTE: `email` is exceptionally exposed as a theme object in
121
121
  # the customers' reset password template
122
122
  check_object(info, all_global_objects + ['email'])
123
+ elsif 'templates/robots.txt' == name
124
+ # NOTE: `robots` is the only object exposed object in
125
+ # the robots.txt template
126
+ check_object(info, ['robots'])
123
127
  elsif 'layout/checkout' == name
124
128
  # NOTE: Shopify Plus has exceptionally exposed objects in
125
129
  # the checkout template
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class CompletionContext
6
+ include PositionHelper
7
+
8
+ attr_reader :storage, :relative_path, :line, :col
9
+
10
+ def initialize(storage, relative_path, line, col)
11
+ @storage = storage
12
+ @relative_path = relative_path
13
+ @line = line
14
+ @col = col
15
+ end
16
+
17
+ def buffer
18
+ @buffer ||= storage.read(relative_path)
19
+ end
20
+
21
+ def buffer_until_previous_row
22
+ @buffer_without_current_row ||= buffer[0..absolute_cursor].lines[0...-1].join
23
+ end
24
+
25
+ def absolute_cursor
26
+ @absolute_cursor ||= from_row_column_to_index(buffer, line, col)
27
+ end
28
+
29
+ def cursor
30
+ @cursor ||= absolute_cursor - token&.start || 0
31
+ end
32
+
33
+ def content
34
+ @content ||= token&.content
35
+ end
36
+
37
+ def token
38
+ @token ||= Tokens.new(buffer).find do |t|
39
+ # Here we include the next character and exclude the first
40
+ # one becase when we want to autocomplete inside a token
41
+ # and at most 1 outside it since the cursor could be placed
42
+ # at the end of the token.
43
+ t.start < absolute_cursor && absolute_cursor <= t.end
44
+ end
45
+ end
46
+
47
+ def clone_and_overwrite(col:)
48
+ CompletionContext.new(storage, relative_path, line, col)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -3,35 +3,29 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class CompletionEngine
6
- include PositionHelper
7
-
8
- def initialize(storage)
6
+ def initialize(storage, bridge = nil)
9
7
  @storage = storage
8
+ @bridge = bridge
10
9
  @providers = CompletionProvider.all.map { |x| x.new(storage) }
11
10
  end
12
11
 
13
12
  def completions(relative_path, line, col)
14
- buffer = @storage.read(relative_path)
15
- cursor = from_row_column_to_index(buffer, line, col)
16
- token = find_token(buffer, cursor)
17
- return [] if token.nil?
13
+ context = context(relative_path, line, col)
14
+
15
+ @providers
16
+ .flat_map { |provider| provider.completions(context) }
17
+ .uniq { |completion_item| completion_item[:label] }
18
+ rescue StandardError => error
19
+ @bridge || raise(error)
20
+
21
+ message = error.message
22
+ backtrace = error.backtrace.join("\n")
18
23
 
19
- @providers.flat_map do |p|
20
- p.completions(
21
- token.content,
22
- cursor - token.start
23
- )
24
- end
24
+ @bridge.log("[completion error] error: #{message}\n#{backtrace}")
25
25
  end
26
26
 
27
- def find_token(buffer, cursor)
28
- Tokens.new(buffer).find do |token|
29
- # Here we include the next character and exclude the first
30
- # one becase when we want to autocomplete inside a token
31
- # and at most 1 outside it since the cursor could be placed
32
- # at the end of the token.
33
- token.start < cursor && cursor <= token.end
34
- end
27
+ def context(relative_path, line, col)
28
+ CompletionContext.new(@storage, relative_path, line, col)
35
29
  end
36
30
  end
37
31
  end
@@ -6,6 +6,10 @@ module ThemeCheck
6
6
  include CompletionHelper
7
7
  include RegexHelpers
8
8
 
9
+ attr_reader :storage
10
+
11
+ CurrentToken = Struct.new(:content, :cursor, :absolute_cursor, :buffer)
12
+
9
13
  class << self
10
14
  def all
11
15
  @all ||= []
@@ -20,9 +24,20 @@ module ThemeCheck
20
24
  @storage = storage
21
25
  end
22
26
 
23
- def completions(content, cursor)
27
+ def completions(relative_path, line, col)
24
28
  raise NotImplementedError
25
29
  end
30
+
31
+ def doc_hash(content)
32
+ return {} if content.nil? || content.empty?
33
+
34
+ {
35
+ documentation: {
36
+ kind: MarkupKinds::MARKDOWN,
37
+ value: content,
38
+ },
39
+ }
40
+ end
26
41
  end
27
42
  end
28
43
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class AssignmentsCompletionProvider < CompletionProvider
6
+ def completions(context)
7
+ content = context.buffer_until_previous_row
8
+
9
+ return [] if content.nil?
10
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
11
+ return [] unless variable_lookup.lookups.empty?
12
+ return [] if context.content[context.cursor - 1] == "."
13
+
14
+ finder = VariableLookupFinder::AssignmentsFinder.new(content)
15
+ finder.find!
16
+
17
+ finder.assignments.map do |label, potential_lookup|
18
+ object, _property = VariableLookupTraverser.lookup_object_and_property(potential_lookup)
19
+ object_to_completion(label, object.name)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def object_to_completion(label, object)
26
+ content = ShopifyLiquid::Documentation.object_doc(object)
27
+
28
+ {
29
+ label: label,
30
+ kind: CompletionItemKinds::VARIABLE,
31
+ **doc_hash(content),
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -4,11 +4,19 @@ module ThemeCheck
4
4
  module LanguageServer
5
5
  class FilterCompletionProvider < CompletionProvider
6
6
  NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o
7
+ FILTER_SEPARATOR_INCLUDING_SPACES = /\s*#{Liquid::FilterSeparator}/
8
+ INPUT_TYPE_VARIABLE = 'variable'
7
9
 
8
- def completions(content, cursor)
10
+ def completions(context)
11
+ content = context.content
12
+ cursor = context.cursor
13
+
14
+ return [] if content.nil?
9
15
  return [] unless can_complete?(content, cursor)
10
- available_labels
11
- .select { |w| w.start_with?(partial(content, cursor)) }
16
+
17
+ context = context_with_cursor_before_potential_filter_separator(context)
18
+ available_filters_for(determine_input_type(context))
19
+ .select { |w| w.name.start_with?(partial(content, cursor)) }
12
20
  .map { |filter| filter_to_completion(filter) }
13
21
  end
14
22
 
@@ -21,12 +29,41 @@ module ThemeCheck
21
29
 
22
30
  private
23
31
 
24
- def available_labels
25
- @labels ||= ShopifyLiquid::Filter.labels - ShopifyLiquid::DeprecatedFilter.labels
32
+ def context_with_cursor_before_potential_filter_separator(context)
33
+ content = context.content
34
+ diff = content.index(FILTER_SEPARATOR_INCLUDING_SPACES) - context.cursor
35
+
36
+ return context unless content.scan(FILTER_SEPARATOR_INCLUDING_SPACES).size == 1
37
+
38
+ context.clone_and_overwrite(col: context.col + diff)
39
+ end
40
+
41
+ def determine_input_type(context)
42
+ variable_lookup = VariableLookupFinder.lookup(context)
43
+
44
+ if variable_lookup
45
+ object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
46
+ return property.return_type if property
47
+ return object.name if object
48
+ end
49
+ end
50
+
51
+ def available_filters_for(input_type)
52
+ filters = ShopifyLiquid::SourceIndex.filters
53
+ .select { |filter| input_type.nil? || filter.input_type == input_type }
54
+ return all_labels if filters.empty?
55
+ return filters if input_type == INPUT_TYPE_VARIABLE
56
+
57
+ filters + available_filters_for(INPUT_TYPE_VARIABLE)
58
+ end
59
+
60
+ def all_labels
61
+ available_filters_for(nil)
26
62
  end
27
63
 
28
64
  def cursor_on_filter?(content, cursor)
29
65
  return false unless content.match?(NAMED_FILTER)
66
+
30
67
  matches(content, NAMED_FILTER).any? do |match|
31
68
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
32
69
  end
@@ -34,17 +71,23 @@ module ThemeCheck
34
71
 
35
72
  def partial(content, cursor)
36
73
  return '' unless content.match?(NAMED_FILTER)
74
+
37
75
  partial_match = matches(content, NAMED_FILTER).find do |match|
38
76
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
39
77
  end
40
78
  return '' if partial_match.nil?
79
+
41
80
  partial_match[1]
42
81
  end
43
82
 
44
83
  def filter_to_completion(filter)
84
+ content = ShopifyLiquid::Documentation.render_doc(filter)
85
+
45
86
  {
46
- label: filter,
87
+ label: filter.name,
47
88
  kind: CompletionItemKinds::FUNCTION,
89
+ tags: filter.deprecated? ? [CompletionItemTag::DEPRECATED] : [],
90
+ **doc_hash(content),
48
91
  }
49
92
  end
50
93
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ObjectAttributeCompletionProvider < CompletionProvider
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
11
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
12
+ return [] if content[cursor - 1] == "." && content[cursor - 2] == "."
13
+
14
+ # Navigate through lookups until the last valid [object, property] level
15
+ object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
16
+
17
+ # If the last lookup level is incomplete/invalid, use the partial term
18
+ # to filter object properties.
19
+ partial = partial_property_name(property, variable_lookup)
20
+
21
+ return [] unless object
22
+
23
+ object
24
+ .properties
25
+ .select { |prop| partial.nil? || prop.name.start_with?(partial) }
26
+ .map { |prop| property_to_completion(prop) }
27
+ end
28
+
29
+ private
30
+
31
+ def partial_property_name(property, variable_lookup)
32
+ last_property = variable_lookup.lookups.last
33
+ last_property if last_property != property&.name
34
+ end
35
+
36
+ def property_to_completion(prop)
37
+ content = ShopifyLiquid::Documentation.render_doc(prop)
38
+
39
+ {
40
+ label: prop.name,
41
+ kind: CompletionItemKinds::PROPERTY,
42
+ **doc_hash(content),
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,19 +3,19 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class ObjectCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
7
- return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
6
+ def completions(context)
7
+ content = context.content
8
+
9
+ return [] if content.nil?
10
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
8
11
  return [] unless variable_lookup.lookups.empty?
9
- return [] if content[cursor - 1] == "."
12
+ return [] if content[context.cursor - 1] == "."
13
+
10
14
  ShopifyLiquid::Object.labels
11
15
  .select { |w| w.start_with?(partial(variable_lookup)) }
12
16
  .map { |object| object_to_completion(object) }
13
17
  end
14
18
 
15
- def variable_lookup_at_cursor(content, cursor)
16
- VariableLookupFinder.lookup(content, cursor)
17
- end
18
-
19
19
  def partial(variable_lookup)
20
20
  variable_lookup.name || ''
21
21
  end
@@ -23,9 +23,12 @@ module ThemeCheck
23
23
  private
24
24
 
25
25
  def object_to_completion(object)
26
+ content = ShopifyLiquid::Documentation.object_doc(object)
27
+
26
28
  {
27
29
  label: object,
28
30
  kind: CompletionItemKinds::VARIABLE,
31
+ **doc_hash(content),
29
32
  }
30
33
  end
31
34
  end
@@ -3,7 +3,11 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class RenderSnippetCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
7
11
  return [] unless cursor_on_quoted_argument?(content, cursor)
8
12
  partial = snippet(content) || ''
9
13
  snippets
@@ -3,7 +3,11 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class TagCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
7
11
  return [] unless can_complete?(content, cursor)
8
12
  partial = first_word(content) || ''
9
13
  labels = ShopifyLiquid::Tag.labels
@@ -23,9 +27,12 @@ module ThemeCheck
23
27
  private
24
28
 
25
29
  def tag_to_completion(tag)
30
+ content = ShopifyLiquid::Documentation.tag_doc(tag)
31
+
26
32
  {
27
33
  label: tag,
28
34
  kind: CompletionItemKinds::KEYWORD,
35
+ **doc_hash(content),
29
36
  }
30
37
  end
31
38
  end
@@ -67,7 +67,7 @@ module ThemeCheck
67
67
  @bridge.supports_work_done_progress = @client_capabilities.supports_work_done_progress?
68
68
  @storage = in_memory_storage(@root_path)
69
69
  @diagnostics_manager = DiagnosticsManager.new
70
- @completion_engine = CompletionEngine.new(@storage)
70
+ @completion_engine = CompletionEngine.new(@storage, @bridge)
71
71
  @document_link_engine = DocumentLinkEngine.new(@storage)
72
72
  @diagnostics_engine = DiagnosticsEngine.new(@storage, @bridge, @diagnostics_manager)
73
73
  @execute_command_engine = ExecuteCommandEngine.new
@@ -90,6 +90,8 @@ module ThemeCheck
90
90
 
91
91
  @configuration.fetch
92
92
  @configuration.register_did_change_capability
93
+
94
+ ShopifyLiquid::SourceManager.download_or_refresh_files
93
95
  end
94
96
 
95
97
  def on_shutdown(id, _params)
@@ -41,5 +41,14 @@ module ThemeCheck
41
41
  module ErrorCodes
42
42
  INTERNAL_ERROR = -32603
43
43
  end
44
+
45
+ module MarkupKinds
46
+ PLAIN_TEXT = 'plaintext'
47
+ MARKDOWN = 'markdown'
48
+ end
49
+
50
+ module CompletionItemTag
51
+ DEPRECATED = 1
52
+ end
44
53
  end
45
54
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module TypeHelper
6
+ def input_type_of(literal)
7
+ case literal
8
+ when String
9
+ 'string'
10
+ when Numeric
11
+ 'number'
12
+ when TrueClass, FalseClass
13
+ 'boolean'
14
+ when NilClass
15
+ 'nil'
16
+ else
17
+ 'untyped'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ class AssignmentsFinder
7
+ class NodeHandler
8
+ def on_assign(node, scope)
9
+ # When a variable is redefined in a new scope we
10
+ # no longer can guarantee the type in the global scope
11
+ #
12
+ # Example:
13
+ # ```liquid
14
+ # {%- liquid
15
+ # assign var1 = some_value
16
+ #
17
+ # if condition
18
+ # assign var1 = another_value
19
+ # ^^^^ from here we no longer can guarantee
20
+ # the type of `var1` in the global scope
21
+ # -%}
22
+ # ```
23
+ p_scope = scope
24
+ while (p_scope = p_scope.parent)
25
+ p_scope.variables.delete(node.value.to)
26
+ end
27
+
28
+ scope << node
29
+ scope
30
+ end
31
+
32
+ ##
33
+ # Table row tags do not rely on blocks to define scopes,
34
+ # so we index their value here
35
+ def on_table_row(node, scope)
36
+ scope = scope.new_child
37
+
38
+ scope << node
39
+ scope
40
+ end
41
+
42
+ ##
43
+ # Define a new scope every time a new block is created
44
+ def on_block_body(node, scope)
45
+ scope = scope.new_child
46
+
47
+ ##
48
+ # 'for' tags handle blocks flattenly and differently
49
+ # than the other tags (if, unless, case).
50
+ #
51
+ # The scope of 'for' tags exists only in the first
52
+ # block, as the following one refers to the else
53
+ # statement of the iteration.
54
+ parent = node.parent
55
+
56
+ scope << parent if parent.type_name == :for && parent.children.first == node
57
+ scope
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ class AssignmentsFinder
7
+ class Scope < Struct.new(:variables, :parent)
8
+ include TypeHelper
9
+
10
+ def new_child
11
+ child_scope = dup
12
+ child_scope.variables = variables.dup
13
+ child_scope.parent = self
14
+ child_scope
15
+ end
16
+
17
+ def <<(node)
18
+ tag = node.value
19
+
20
+ case tag
21
+ when Liquid::Assign
22
+ variable_name = tag.to
23
+ variables[variable_name] = assign_tag_as_potential_lookup(tag)
24
+ when Liquid::For, Liquid::TableRow
25
+ variable_name = tag.variable_name
26
+ variables[variable_name] = iteration_tag_as_potential_lookup(tag)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def assign_tag_as_potential_lookup(tag)
33
+ variable_lookup = tag.from.name
34
+
35
+ unless variable_lookup.is_a?(Liquid::VariableLookup)
36
+ return PotentialLookup.new(input_type_of(variable_lookup), [], variables)
37
+ end
38
+
39
+ name = variable_lookup.name
40
+ lookups = variable_lookup.lookups
41
+
42
+ PotentialLookup.new(name, lookups, variables)
43
+ end
44
+
45
+ def iteration_tag_as_potential_lookup(tag)
46
+ variable_lookup = tag.collection_name
47
+
48
+ name = variable_lookup.name
49
+ lookups = [*variable_lookup.lookups, 'first']
50
+
51
+ PotentialLookup.new(name, lookups, variables)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ module VariableLookupFinder
6
+ class AssignmentsFinder
7
+ class ScopeVisitor
8
+ attr_reader :global_scope, :current_scope
9
+
10
+ def initialize
11
+ @node_handler = NodeHandler.new
12
+ @global_scope = Scope.new({})
13
+ @current_scope = Scope.new({})
14
+ end
15
+
16
+ def visit_template(template)
17
+ return unless template
18
+
19
+ visit(liquid_node(template), global_scope)
20
+ end
21
+
22
+ private
23
+
24
+ def visit(node, scope)
25
+ return if node.type_name == :variable_lookup
26
+
27
+ method = :"on_#{node.type_name}"
28
+ scope = @node_handler.send(method, node, scope) if @node_handler.respond_to?(method)
29
+
30
+ @current_scope = scope
31
+
32
+ node.children.each { |child| visit(child, scope) }
33
+ end
34
+
35
+ def liquid_node(template)
36
+ LiquidNode.new(template.root, nil, template)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end