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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +82 -0
- data/README.md +4 -0
- data/Rakefile +7 -0
- data/TROUBLESHOOTING.md +65 -0
- data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
- data/data/shopify_liquid/documentation/filters.json +5528 -0
- data/data/shopify_liquid/documentation/latest.json +1 -0
- data/data/shopify_liquid/documentation/objects.json +19272 -0
- data/data/shopify_liquid/documentation/tags.json +1252 -0
- data/lib/theme_check/checks/undefined_object.rb +4 -0
- data/lib/theme_check/language_server/completion_context.rb +52 -0
- data/lib/theme_check/language_server/completion_engine.rb +15 -21
- data/lib/theme_check/language_server/completion_provider.rb +16 -1
- data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
- data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
- data/lib/theme_check/language_server/handler.rb +3 -1
- data/lib/theme_check/language_server/protocol.rb +9 -0
- data/lib/theme_check/language_server/type_helper.rb +22 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
- data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
- data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
- data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
- data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
- data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
- data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
- data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
- data/lib/theme_check/language_server.rb +12 -0
- data/lib/theme_check/remote_asset_file.rb +13 -7
- data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
- data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
- data/lib/theme_check/shopify_liquid/filter.rb +4 -0
- data/lib/theme_check/shopify_liquid/object.rb +4 -0
- data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
- data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
- data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
- data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
- data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
- data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
- data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
- data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
- data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
- data/lib/theme_check/shopify_liquid/tag.rb +4 -0
- data/lib/theme_check/shopify_liquid.rb +17 -1
- data/lib/theme_check/version.rb +1 -1
- data/shipit.rubygems.yml +3 -0
- data/theme-check.gemspec +3 -1
- 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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
@
|
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
|
28
|
-
|
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(
|
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(
|
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
|
-
|
11
|
-
|
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
|
25
|
-
|
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
|
data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb
ADDED
@@ -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(
|
7
|
-
|
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
|
data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb
CHANGED
@@ -3,7 +3,11 @@
|
|
3
3
|
module ThemeCheck
|
4
4
|
module LanguageServer
|
5
5
|
class RenderSnippetCompletionProvider < CompletionProvider
|
6
|
-
def completions(
|
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(
|
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)
|
@@ -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
|
data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb
ADDED
@@ -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
|
data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb
ADDED
@@ -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
|