theme-check 1.11.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
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