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.
- 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
|