platformos-check 0.0.1

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 (240) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +2 -0
  3. data/.gitignore +22 -0
  4. data/.rubocop.yml +555 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/CONTRIBUTING.md +209 -0
  8. data/Gemfile +33 -0
  9. data/Guardfile +7 -0
  10. data/LICENSE.md +10 -0
  11. data/Makefile +18 -0
  12. data/README.md +189 -0
  13. data/RELEASING.md +35 -0
  14. data/Rakefile +83 -0
  15. data/TROUBLESHOOTING.md +35 -0
  16. data/bin/platformos-check +29 -0
  17. data/bin/platformos-check-language-server +29 -0
  18. data/config/default.yml +98 -0
  19. data/config/nothing.yml +11 -0
  20. data/data/platformos_liquid/built_in_liquid_objects.json +66 -0
  21. data/data/platformos_liquid/deprecated_filters.json +22 -0
  22. data/data/platformos_liquid/documentation/filters.json +6 -0
  23. data/data/platformos_liquid/documentation/latest.json +2 -0
  24. data/data/platformos_liquid/documentation/objects.json +6 -0
  25. data/data/platformos_liquid/documentation/tags.json +6 -0
  26. data/docker/check.Dockerfile +3 -0
  27. data/docker/lsp.Dockerfile +21 -0
  28. data/docs/api/check.md +15 -0
  29. data/docs/api/html_check.md +46 -0
  30. data/docs/api/liquid_check.md +115 -0
  31. data/docs/api/yaml_check.md +19 -0
  32. data/docs/checks/TEMPLATE.md.erb +52 -0
  33. data/docs/checks/convert_include_to_render.md +48 -0
  34. data/docs/checks/deprecated_filter.md +30 -0
  35. data/docs/checks/html_parsing_error.md +50 -0
  36. data/docs/checks/img_lazy_loading.md +63 -0
  37. data/docs/checks/img_width_and_height.md +79 -0
  38. data/docs/checks/invalid_args.md +56 -0
  39. data/docs/checks/liquid_tag.md +65 -0
  40. data/docs/checks/missing_enable_comment.md +50 -0
  41. data/docs/checks/missing_template.md +65 -0
  42. data/docs/checks/parse_json_format.md +76 -0
  43. data/docs/checks/parser_blocking_javascript.md +97 -0
  44. data/docs/checks/required_layout_object.md +28 -0
  45. data/docs/checks/space_inside_braces.md +89 -0
  46. data/docs/checks/syntax_error.md +49 -0
  47. data/docs/checks/template_length.md +45 -0
  48. data/docs/checks/undefined_object.md +71 -0
  49. data/docs/checks/unknown_filter.md +46 -0
  50. data/docs/checks/unused_assign.md +63 -0
  51. data/docs/checks/unused_partial.md +32 -0
  52. data/docs/checks/valid_yaml.md +48 -0
  53. data/docs/flamegraph.svg +18488 -0
  54. data/docs/language_server/code-action-command-palette.png +0 -0
  55. data/docs/language_server/code-action-flow.png +0 -0
  56. data/docs/language_server/code-action-keyboard.png +0 -0
  57. data/docs/language_server/code-action-light-bulb.png +0 -0
  58. data/docs/language_server/code-action-problem.png +0 -0
  59. data/docs/language_server/code-action-quickfix.png +0 -0
  60. data/docs/language_server/how_to_correct_code_with_code_actions_and_execute_command.md +197 -0
  61. data/docs/preview.png +0 -0
  62. data/exe/platformos-check +6 -0
  63. data/exe/platformos-check-language-server +7 -0
  64. data/lib/platformos_check/analyzer.rb +178 -0
  65. data/lib/platformos_check/api_call_file.rb +9 -0
  66. data/lib/platformos_check/app.rb +138 -0
  67. data/lib/platformos_check/app_file.rb +89 -0
  68. data/lib/platformos_check/app_file_rewriter.rb +79 -0
  69. data/lib/platformos_check/asset_file.rb +34 -0
  70. data/lib/platformos_check/bug.rb +23 -0
  71. data/lib/platformos_check/check.rb +163 -0
  72. data/lib/platformos_check/checks/TEMPLATE.rb.erb +11 -0
  73. data/lib/platformos_check/checks/convert_include_to_render.rb +17 -0
  74. data/lib/platformos_check/checks/deprecated_filter.rb +123 -0
  75. data/lib/platformos_check/checks/html_parsing_error.rb +13 -0
  76. data/lib/platformos_check/checks/img_lazy_loading.rb +18 -0
  77. data/lib/platformos_check/checks/img_width_and_height.rb +46 -0
  78. data/lib/platformos_check/checks/invalid_args.rb +81 -0
  79. data/lib/platformos_check/checks/liquid_tag.rb +47 -0
  80. data/lib/platformos_check/checks/missing_enable_comment.rb +37 -0
  81. data/lib/platformos_check/checks/missing_template.rb +107 -0
  82. data/lib/platformos_check/checks/parse_json_format.rb +31 -0
  83. data/lib/platformos_check/checks/parser_blocking_javascript.rb +17 -0
  84. data/lib/platformos_check/checks/required_layout_object.rb +41 -0
  85. data/lib/platformos_check/checks/space_inside_braces.rb +150 -0
  86. data/lib/platformos_check/checks/syntax_error.rb +31 -0
  87. data/lib/platformos_check/checks/template_length.rb +20 -0
  88. data/lib/platformos_check/checks/undefined_object.rb +206 -0
  89. data/lib/platformos_check/checks/unknown_filter.rb +27 -0
  90. data/lib/platformos_check/checks/unused_assign.rb +101 -0
  91. data/lib/platformos_check/checks/unused_partial.rb +93 -0
  92. data/lib/platformos_check/checks/valid_yaml.rb +16 -0
  93. data/lib/platformos_check/checks.rb +73 -0
  94. data/lib/platformos_check/checks_tracking.rb +9 -0
  95. data/lib/platformos_check/cli.rb +239 -0
  96. data/lib/platformos_check/config.rb +219 -0
  97. data/lib/platformos_check/config_file.rb +6 -0
  98. data/lib/platformos_check/corrector.rb +68 -0
  99. data/lib/platformos_check/disabled_check.rb +44 -0
  100. data/lib/platformos_check/disabled_checks.rb +96 -0
  101. data/lib/platformos_check/email_file.rb +9 -0
  102. data/lib/platformos_check/exceptions.rb +36 -0
  103. data/lib/platformos_check/file_system_storage.rb +93 -0
  104. data/lib/platformos_check/graphql_file.rb +68 -0
  105. data/lib/platformos_check/html_check.rb +8 -0
  106. data/lib/platformos_check/html_node.rb +210 -0
  107. data/lib/platformos_check/html_visitor.rb +36 -0
  108. data/lib/platformos_check/in_memory_storage.rb +68 -0
  109. data/lib/platformos_check/json_file.rb +57 -0
  110. data/lib/platformos_check/json_helper.rb +73 -0
  111. data/lib/platformos_check/json_helpers.rb +24 -0
  112. data/lib/platformos_check/json_printer.rb +32 -0
  113. data/lib/platformos_check/language_server/bridge.rb +167 -0
  114. data/lib/platformos_check/language_server/channel.rb +69 -0
  115. data/lib/platformos_check/language_server/client_capabilities.rb +27 -0
  116. data/lib/platformos_check/language_server/code_action_engine.rb +32 -0
  117. data/lib/platformos_check/language_server/code_action_provider.rb +41 -0
  118. data/lib/platformos_check/language_server/code_action_providers/quickfix_code_action_provider.rb +85 -0
  119. data/lib/platformos_check/language_server/code_action_providers/source_fix_all_code_action_provider.rb +41 -0
  120. data/lib/platformos_check/language_server/completion_context.rb +52 -0
  121. data/lib/platformos_check/language_server/completion_engine.rb +32 -0
  122. data/lib/platformos_check/language_server/completion_helper.rb +26 -0
  123. data/lib/platformos_check/language_server/completion_provider.rb +53 -0
  124. data/lib/platformos_check/language_server/completion_providers/assignments_completion_provider.rb +40 -0
  125. data/lib/platformos_check/language_server/completion_providers/filter_completion_provider.rb +102 -0
  126. data/lib/platformos_check/language_server/completion_providers/object_attribute_completion_provider.rb +48 -0
  127. data/lib/platformos_check/language_server/completion_providers/object_completion_provider.rb +38 -0
  128. data/lib/platformos_check/language_server/completion_providers/render_snippet_completion_provider.rb +50 -0
  129. data/lib/platformos_check/language_server/completion_providers/tag_completion_provider.rb +41 -0
  130. data/lib/platformos_check/language_server/configuration.rb +89 -0
  131. data/lib/platformos_check/language_server/constants.rb +29 -0
  132. data/lib/platformos_check/language_server/diagnostic.rb +129 -0
  133. data/lib/platformos_check/language_server/diagnostics_engine.rb +131 -0
  134. data/lib/platformos_check/language_server/diagnostics_manager.rb +184 -0
  135. data/lib/platformos_check/language_server/document_change_corrector.rb +271 -0
  136. data/lib/platformos_check/language_server/document_link_engine.rb +21 -0
  137. data/lib/platformos_check/language_server/document_link_provider.rb +71 -0
  138. data/lib/platformos_check/language_server/document_link_providers/asset_document_link_provider.rb +11 -0
  139. data/lib/platformos_check/language_server/document_link_providers/include_document_link_provider.rb +11 -0
  140. data/lib/platformos_check/language_server/document_link_providers/render_document_link_provider.rb +11 -0
  141. data/lib/platformos_check/language_server/document_link_providers/section_document_link_provider.rb +11 -0
  142. data/lib/platformos_check/language_server/execute_command_engine.rb +19 -0
  143. data/lib/platformos_check/language_server/execute_command_provider.rb +30 -0
  144. data/lib/platformos_check/language_server/execute_command_providers/correction_execute_command_provider.rb +48 -0
  145. data/lib/platformos_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +28 -0
  146. data/lib/platformos_check/language_server/handler.rb +310 -0
  147. data/lib/platformos_check/language_server/hover_engine.rb +32 -0
  148. data/lib/platformos_check/language_server/hover_provider.rb +53 -0
  149. data/lib/platformos_check/language_server/hover_providers/filter_hover_provider.rb +113 -0
  150. data/lib/platformos_check/language_server/io_messenger.rb +109 -0
  151. data/lib/platformos_check/language_server/messenger.rb +27 -0
  152. data/lib/platformos_check/language_server/protocol.rb +55 -0
  153. data/lib/platformos_check/language_server/server.rb +188 -0
  154. data/lib/platformos_check/language_server/tokens.rb +55 -0
  155. data/lib/platformos_check/language_server/type_helper.rb +22 -0
  156. data/lib/platformos_check/language_server/uri_helper.rb +39 -0
  157. data/lib/platformos_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +87 -0
  158. data/lib/platformos_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +60 -0
  159. data/lib/platformos_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +44 -0
  160. data/lib/platformos_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  161. data/lib/platformos_check/language_server/variable_lookup_finder/constants.rb +44 -0
  162. data/lib/platformos_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  163. data/lib/platformos_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  164. data/lib/platformos_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
  165. data/lib/platformos_check/language_server/variable_lookup_finder.rb +262 -0
  166. data/lib/platformos_check/language_server/variable_lookup_traverser.rb +70 -0
  167. data/lib/platformos_check/language_server/versioned_in_memory_storage.rb +84 -0
  168. data/lib/platformos_check/language_server.rb +71 -0
  169. data/lib/platformos_check/layout_file.rb +15 -0
  170. data/lib/platformos_check/liquid_check.rb +10 -0
  171. data/lib/platformos_check/liquid_file.rb +102 -0
  172. data/lib/platformos_check/liquid_node.rb +570 -0
  173. data/lib/platformos_check/liquid_visitor.rb +39 -0
  174. data/lib/platformos_check/migration_file.rb +9 -0
  175. data/lib/platformos_check/node.rb +53 -0
  176. data/lib/platformos_check/offense.rb +228 -0
  177. data/lib/platformos_check/page_file.rb +9 -0
  178. data/lib/platformos_check/parsing_helpers.rb +21 -0
  179. data/lib/platformos_check/partial_file.rb +15 -0
  180. data/lib/platformos_check/platformos_liquid/deprecated_filter.rb +31 -0
  181. data/lib/platformos_check/platformos_liquid/documentation/markdown_template.rb +51 -0
  182. data/lib/platformos_check/platformos_liquid/documentation.rb +45 -0
  183. data/lib/platformos_check/platformos_liquid/filter.rb +19 -0
  184. data/lib/platformos_check/platformos_liquid/object.rb +15 -0
  185. data/lib/platformos_check/platformos_liquid/source_index/base_entry.rb +66 -0
  186. data/lib/platformos_check/platformos_liquid/source_index/base_state.rb +23 -0
  187. data/lib/platformos_check/platformos_liquid/source_index/filter_entry.rb +26 -0
  188. data/lib/platformos_check/platformos_liquid/source_index/filter_state.rb +11 -0
  189. data/lib/platformos_check/platformos_liquid/source_index/object_entry.rb +20 -0
  190. data/lib/platformos_check/platformos_liquid/source_index/object_state.rb +11 -0
  191. data/lib/platformos_check/platformos_liquid/source_index/parameter_entry.rb +25 -0
  192. data/lib/platformos_check/platformos_liquid/source_index/property_entry.rb +21 -0
  193. data/lib/platformos_check/platformos_liquid/source_index/return_type_entry.rb +41 -0
  194. data/lib/platformos_check/platformos_liquid/source_index/tag_entry.rb +24 -0
  195. data/lib/platformos_check/platformos_liquid/source_index/tag_state.rb +11 -0
  196. data/lib/platformos_check/platformos_liquid/source_index.rb +79 -0
  197. data/lib/platformos_check/platformos_liquid/source_manager.rb +116 -0
  198. data/lib/platformos_check/platformos_liquid/tag.rb +59 -0
  199. data/lib/platformos_check/platformos_liquid.rb +21 -0
  200. data/lib/platformos_check/position.rb +180 -0
  201. data/lib/platformos_check/position_helper.rb +57 -0
  202. data/lib/platformos_check/printer.rb +87 -0
  203. data/lib/platformos_check/regex_helpers.rb +21 -0
  204. data/lib/platformos_check/releaser.rb +43 -0
  205. data/lib/platformos_check/schema_file.rb +6 -0
  206. data/lib/platformos_check/sms_file.rb +9 -0
  207. data/lib/platformos_check/storage.rb +29 -0
  208. data/lib/platformos_check/string_helpers.rb +48 -0
  209. data/lib/platformos_check/tags/background.rb +67 -0
  210. data/lib/platformos_check/tags/base.rb +14 -0
  211. data/lib/platformos_check/tags/base_block.rb +14 -0
  212. data/lib/platformos_check/tags/base_tag_methods.rb +59 -0
  213. data/lib/platformos_check/tags/cache.rb +13 -0
  214. data/lib/platformos_check/tags/export.rb +30 -0
  215. data/lib/platformos_check/tags/form.rb +19 -0
  216. data/lib/platformos_check/tags/function.rb +58 -0
  217. data/lib/platformos_check/tags/graphql.rb +70 -0
  218. data/lib/platformos_check/tags/hash_assign.rb +75 -0
  219. data/lib/platformos_check/tags/log.rb +15 -0
  220. data/lib/platformos_check/tags/parse_json.rb +24 -0
  221. data/lib/platformos_check/tags/print.rb +20 -0
  222. data/lib/platformos_check/tags/redirect_to.rb +15 -0
  223. data/lib/platformos_check/tags/render.rb +60 -0
  224. data/lib/platformos_check/tags/response_headers.rb +20 -0
  225. data/lib/platformos_check/tags/response_status.rb +20 -0
  226. data/lib/platformos_check/tags/return.rb +20 -0
  227. data/lib/platformos_check/tags/session.rb +27 -0
  228. data/lib/platformos_check/tags/sign_in.rb +27 -0
  229. data/lib/platformos_check/tags/spam_protection.rb +15 -0
  230. data/lib/platformos_check/tags/theme_render.rb +58 -0
  231. data/lib/platformos_check/tags/try.rb +59 -0
  232. data/lib/platformos_check/tags.rb +65 -0
  233. data/lib/platformos_check/translation_file.rb +6 -0
  234. data/lib/platformos_check/user_schema_file.rb +6 -0
  235. data/lib/platformos_check/version.rb +5 -0
  236. data/lib/platformos_check/yaml_check.rb +11 -0
  237. data/lib/platformos_check/yaml_file.rb +57 -0
  238. data/lib/platformos_check.rb +106 -0
  239. data/platformos-check.gemspec +34 -0
  240. metadata +329 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ class LayoutFile < LiquidFile
5
+ DIR_PREFIX = %r{\A/?((marketplace_builder|app)/(views/layouts)/|modules/((\w|-)*)/(private|public)/(views/layouts)/)}
6
+
7
+ def layout?
8
+ true
9
+ end
10
+
11
+ def dir_prefix
12
+ DIR_PREFIX
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parsing_helpers"
4
+
5
+ module PlatformosCheck
6
+ class LiquidCheck < Check
7
+ extend ChecksTracking
8
+ include ParsingHelpers
9
+ end
10
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ class LiquidFile < AppFile
5
+ def write
6
+ content = rewriter.to_s
7
+ return unless source != content
8
+
9
+ @storage.write(@relative_path, content.gsub("\n", @eol))
10
+ @source = content
11
+ @rewriter = nil
12
+ end
13
+
14
+ def liquid?
15
+ true
16
+ end
17
+
18
+ def template?
19
+ name.start_with?('template')
20
+ end
21
+
22
+ def notification?
23
+ false
24
+ end
25
+
26
+ def migration?
27
+ false
28
+ end
29
+
30
+ def page?
31
+ false
32
+ end
33
+
34
+ def partial?
35
+ false
36
+ end
37
+
38
+ def layout?
39
+ false
40
+ end
41
+
42
+ def section?
43
+ name.start_with?('sections')
44
+ end
45
+
46
+ def snippet?
47
+ name.start_with?('snippet')
48
+ end
49
+
50
+ def rewriter
51
+ @rewriter ||= AppFileRewriter.new(@relative_path, source)
52
+ end
53
+
54
+ def source_excerpt(line)
55
+ original_lines = source.split("\n")
56
+ original_lines[bounded(0, line - 1, original_lines.size - 1)].strip
57
+ rescue StandardError => e
58
+ PlatformosCheck.bug(<<~EOS)
59
+ Exception while running `source_excerpt(#{line})`:
60
+ ```
61
+ #{e.class}: #{e.message}
62
+ #{e.backtrace.join("\n ")}
63
+ ```
64
+
65
+ path: #{path}
66
+
67
+ source:
68
+ ```
69
+ #{source}
70
+ ```
71
+ EOS
72
+ end
73
+
74
+ def parse
75
+ @ast ||= self.class.parse(source)
76
+ end
77
+
78
+ def warnings
79
+ @ast.warnings
80
+ end
81
+
82
+ def root
83
+ parse.root
84
+ end
85
+
86
+ def self.parse(source)
87
+ Tags.register_tags!
88
+ Liquid::Template.parse(
89
+ source,
90
+ line_numbers: true,
91
+ error_mode: :warn,
92
+ disable_liquid_c_nodes: true
93
+ )
94
+ end
95
+
96
+ private
97
+
98
+ def bounded(lower, x, upper)
99
+ [lower, [x, upper].min].max
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,570 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ # A node from the Liquid AST, the result of parsing a liquid file.
5
+ class LiquidNode < Node
6
+ attr_reader :value, :parent, :app_file
7
+
8
+ def initialize(value, parent, app_file)
9
+ raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(LiquidNode)
10
+
11
+ @value = value
12
+ @parent = parent
13
+ @app_file = app_file
14
+ @tag_markup = nil
15
+ @line_number_offset = 0
16
+ end
17
+
18
+ # Array of children nodes.
19
+ def children
20
+ @children ||= begin
21
+ nodes =
22
+ if comment?
23
+ []
24
+ elsif defined?(@value.class::ParseTreeVisitor)
25
+ @value.class::ParseTreeVisitor.new(@value, {}).children
26
+ elsif @value.respond_to?(:nodelist)
27
+ Array(@value.nodelist)
28
+ else
29
+ []
30
+ end
31
+ # Work around a bug in Liquid::Variable::ParseTreeVisitor that doesn't return
32
+ # the args in a hash as children nodes.
33
+ nodes = nodes.flat_map do |node|
34
+ case node
35
+ when Hash
36
+ node.values
37
+ else
38
+ node
39
+ end
40
+ end
41
+ nodes
42
+ .reject(&:nil?) # We don't want nil nodes, and they can happen
43
+ .map { |node| LiquidNode.new(node, self, @app_file) }
44
+ end
45
+ end
46
+
47
+ # The original source code of the node. Doesn't contain wrapping braces.
48
+ def markup
49
+ if tag?
50
+ tag_markup
51
+ elsif literal?
52
+ value.to_s
53
+ elsif @value.instance_variable_defined?(:@markup)
54
+ @value.instance_variable_get(:@markup)
55
+ end
56
+ end
57
+
58
+ # The original source code of the node. Does contain wrapping braces.
59
+ def outer_markup
60
+ if literal?
61
+ markup
62
+ elsif variable_lookup?
63
+ ''
64
+ elsif variable?
65
+ start_token + markup + end_token
66
+ elsif tag? && block?
67
+ start_index = block_start_start_index
68
+ end_index = block_start_end_index
69
+ end_index += inner_markup.size
70
+ end_index = find_block_delimiter(end_index)&.end(0)
71
+ source[start_index...end_index]
72
+ elsif tag?
73
+ source[block_start_start_index...block_start_end_index]
74
+ else
75
+ inner_markup
76
+ end
77
+ end
78
+
79
+ def inner_markup
80
+ return '' unless block?
81
+
82
+ @inner_markup ||= source[block_start_end_index...block_end_start_index]
83
+ end
84
+
85
+ def inner_json
86
+ return nil unless parse_json?
87
+
88
+ @inner_json ||= JSON.parse(inner_markup)
89
+ rescue JSON::ParserError
90
+ # Handled by ValidSchema
91
+ @inner_json = nil
92
+ end
93
+
94
+ def markup=(markup)
95
+ return unless @value.instance_variable_defined?(:@markup)
96
+
97
+ @value.instance_variable_set(:@markup, markup)
98
+ end
99
+
100
+ # Most nodes have a line number, but it's not guaranteed.
101
+ def line_number
102
+ if tag? && @value.respond_to?(:line_number)
103
+ markup # initialize the line_number_offset
104
+ @value.line_number - @line_number_offset
105
+ elsif @value.respond_to?(:line_number)
106
+ @value.line_number
107
+ end
108
+ end
109
+
110
+ def start_index
111
+ position.start_index
112
+ end
113
+
114
+ def start_row
115
+ position.start_row
116
+ end
117
+
118
+ def start_column
119
+ position.start_column
120
+ end
121
+
122
+ def end_index
123
+ position.end_index
124
+ end
125
+
126
+ def end_row
127
+ position.end_row
128
+ end
129
+
130
+ def end_column
131
+ position.end_column
132
+ end
133
+
134
+ # Literals are hard-coded values in the liquid file.
135
+ def literal?
136
+ @value.is_a?(String) || @value.is_a?(Integer)
137
+ end
138
+
139
+ # A {% tag %} node?
140
+ def tag?
141
+ @value.is_a?(Liquid::Tag)
142
+ end
143
+
144
+ def variable?
145
+ @value.is_a?(Liquid::Variable)
146
+ end
147
+
148
+ def assigned_or_echoed_variable?
149
+ variable? && start_token == ""
150
+ end
151
+
152
+ def variable_lookup?
153
+ @value.is_a?(Liquid::VariableLookup)
154
+ end
155
+
156
+ # A {% comment %} block node?
157
+ def comment?
158
+ @value.is_a?(Liquid::Comment)
159
+ end
160
+
161
+ # {% # comment %}
162
+ def inline_comment?
163
+ @value.is_a?(Liquid::InlineComment)
164
+ end
165
+
166
+ # Top level node of every liquid_file.
167
+ def document?
168
+ @value.is_a?(Liquid::Document)
169
+ end
170
+ alias root? document?
171
+
172
+ # A {% tag %}...{% endtag %} node?
173
+ def block_tag?
174
+ @value.is_a?(Liquid::Block)
175
+ end
176
+
177
+ # The body of blocks
178
+ def block_body?
179
+ @value.is_a?(Liquid::BlockBody)
180
+ end
181
+
182
+ # A block of type of node?
183
+ def block?
184
+ block_tag? || block_body? || document?
185
+ end
186
+
187
+ def parse_json?
188
+ @value.is_a?(PlatformosCheck::Tags::ParseJson)
189
+ end
190
+
191
+ def function?
192
+ @value.is_a?(PlatformosCheck::Tags::Function)
193
+ end
194
+
195
+ # The `:under_score_name` of this type of node. Used to dispatch to the `on_<type_name>`
196
+ # and `after_<type_name>` check methods.
197
+ def type_name
198
+ @type_name ||= StringHelpers.underscore(StringHelpers.demodulize(@value.class.name)).to_sym
199
+ end
200
+
201
+ def filters
202
+ raise TypeError, "Attempting to lookup filters of #{type_name}. Only variables have filters." unless variable?
203
+
204
+ @value.filters
205
+ end
206
+
207
+ def source
208
+ app_file&.source
209
+ end
210
+
211
+ # For debugging purposes, this might be easier for the eyes.
212
+ def to_h
213
+ if literal?
214
+ return @value
215
+ elsif variable_lookup?
216
+ return {
217
+ type_name:,
218
+ name: value.name.to_s,
219
+ lookups: children.map(&:to_h)
220
+ }
221
+ end
222
+
223
+ {
224
+ type_name:,
225
+ markup: outer_markup,
226
+ children: children.map(&:to_h)
227
+ }
228
+ end
229
+
230
+ def block_start_markup
231
+ source[block_start_start_index...block_start_end_index]
232
+ end
233
+
234
+ def block_start_start_index
235
+ @block_start_start_index ||= if inside_liquid_tag?
236
+ backtrack_on_whitespace(source, start_index, /[ \t]/)
237
+ elsif tag?
238
+ backtrack_on_whitespace(source, start_index) - start_token.length
239
+ else
240
+ position.start_index - start_token.length
241
+ end
242
+ end
243
+
244
+ def block_start_end_index
245
+ @block_start_end_index ||= position.end_index + end_token.size
246
+ end
247
+
248
+ def block_end_markup
249
+ source[block_end_start_index...block_end_end_index]
250
+ end
251
+
252
+ def block_end_start_index
253
+ return block_start_end_index unless tag? && block?
254
+
255
+ @block_end_start_index ||= block_end_match&.begin(0) || block_start_end_index
256
+ end
257
+
258
+ def block_end_end_index
259
+ return block_end_start_index unless tag? && block?
260
+
261
+ @block_end_end_index ||= block_end_match&.end(0) || block_start_end_index
262
+ end
263
+
264
+ def outer_markup_start_index
265
+ outer_markup_position.start_index
266
+ end
267
+
268
+ def outer_markup_end_index
269
+ outer_markup_position.end_index
270
+ end
271
+
272
+ def outer_markup_start_row
273
+ outer_markup_position.start_row
274
+ end
275
+
276
+ def outer_markup_start_column
277
+ outer_markup_position.start_column
278
+ end
279
+
280
+ def outer_markup_end_row
281
+ outer_markup_position.end_row
282
+ end
283
+
284
+ def outer_markup_end_column
285
+ outer_markup_position.end_column
286
+ end
287
+
288
+ def inner_markup_start_index
289
+ inner_markup_position.start_index
290
+ end
291
+
292
+ def inner_markup_end_index
293
+ inner_markup_position.end_index
294
+ end
295
+
296
+ def inner_markup_start_row
297
+ inner_markup_position.start_row
298
+ end
299
+
300
+ def inner_markup_start_column
301
+ inner_markup_position.start_column
302
+ end
303
+
304
+ def inner_markup_end_row
305
+ inner_markup_position.end_row
306
+ end
307
+
308
+ def inner_markup_end_column
309
+ inner_markup_position.end_column
310
+ end
311
+
312
+ WHITESPACE = /\s/
313
+
314
+ # Is this node inside a `{% liquid ... %}` block?
315
+ def inside_liquid_tag?
316
+ # What we're doing here is starting at the start of the tag and
317
+ # backtrack on all the whitespace until we land on something. If
318
+ # that something is {% or %-, then we can safely assume that
319
+ # we're inside a full tag and not a liquid tag.
320
+ @inside_liquid_tag ||= if tag? && start_index && source
321
+ i = 1
322
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
323
+ first_two_backtracked_characters = source[(start_index - i - 1)..(start_index - i)]
324
+ first_two_backtracked_characters != "{%" && first_two_backtracked_characters != "%-"
325
+ else
326
+ false
327
+ end
328
+ end
329
+
330
+ # Is this node inside a tag or variable that starts by removing whitespace. i.e. {%- or {{-
331
+ def whitespace_trimmed_start?
332
+ @whitespace_trimmed_start ||= if start_index && source && !inside_liquid_tag?
333
+ i = 1
334
+ i += 1 while source[start_index - i] =~ WHITESPACE && i < start_index
335
+ source[start_index - i] == "-"
336
+ else
337
+ false
338
+ end
339
+ end
340
+
341
+ # Is this node inside a tag or variable ends starts by removing whitespace. i.e. -%} or -}}
342
+ def whitespace_trimmed_end?
343
+ @whitespace_trimmed_end ||= if end_index && source && !inside_liquid_tag?
344
+ i = 0
345
+ i += 1 while source[end_index + i] =~ WHITESPACE && i < source.size
346
+ source[end_index + i] == "-"
347
+ else
348
+ false
349
+ end
350
+ end
351
+
352
+ def start_token
353
+ if inside_liquid_tag?
354
+ ""
355
+ elsif variable? && source[start_index - 3..start_index - 1] == "{{-"
356
+ "{{-"
357
+ elsif variable? && source[start_index - 2..start_index - 1] == "{{"
358
+ "{{"
359
+ elsif tag? && whitespace_trimmed_start?
360
+ "{%-"
361
+ elsif tag?
362
+ "{%"
363
+ else
364
+ ""
365
+ end
366
+ end
367
+
368
+ def end_token
369
+ if inside_liquid_tag? && source[end_index] == "\n"
370
+ "\n"
371
+ elsif inside_liquid_tag?
372
+ ""
373
+ elsif variable? && source[end_index...end_index + 3] == "-}}"
374
+ "-}}"
375
+ elsif variable? && source[end_index...end_index + 2] == "}}"
376
+ "}}"
377
+ elsif tag? && whitespace_trimmed_end?
378
+ "-%}"
379
+ elsif tag?
380
+ "%}"
381
+ else # this could happen because we're in an assign statement (variable)
382
+ ""
383
+ end
384
+ end
385
+
386
+ private
387
+
388
+ def position
389
+ @position ||= Position.new(
390
+ markup,
391
+ app_file&.source,
392
+ line_number_1_indexed: line_number
393
+ )
394
+ end
395
+
396
+ def outer_markup_position
397
+ @outer_markup_position ||= StrictPosition.new(
398
+ outer_markup,
399
+ source,
400
+ block_start_start_index
401
+ )
402
+ end
403
+
404
+ def inner_markup_position
405
+ @inner_markup_position ||= StrictPosition.new(
406
+ inner_markup,
407
+ source,
408
+ block_start_end_index
409
+ )
410
+ end
411
+
412
+ # Here we're hacking around a glorious bug in Liquid that makes it so the
413
+ # line_number and markup of a tag is wrong if there's whitespace
414
+ # between the tag_name and the markup of the tag.
415
+ #
416
+ # {%
417
+ # render
418
+ # 'foo'
419
+ # %}
420
+ #
421
+ # Returns a raw value of "render 'foo'\n".
422
+ # The "\n " between render and 'foo' got replaced by a single space.
423
+ #
424
+ # And the line number is the one of 'foo'\n%}. Yay!
425
+ #
426
+ # This breaks any kind of position logic we have since that string
427
+ # does not exist in the app_file.
428
+ def tag_markup
429
+ return @tag_markup if @tag_markup
430
+
431
+ l = 1
432
+ scanner = StringScanner.new(source)
433
+ scanner.scan_until(/\n/) while l < @value.line_number && (l += 1)
434
+ start = scanner.charpos
435
+
436
+ tag_name = @value.tag_name
437
+ tag_markup = @value.instance_variable_get(:@markup)
438
+
439
+ # This is tricky, if the tag_markup is empty, then the tag could
440
+ # either start on a previous line, or the tag could start on the
441
+ # same line.
442
+ #
443
+ # Consider this:
444
+ # 1 {%
445
+ # 2 comment
446
+ # 3 %}{% endcomment %}{%comment%}
447
+ #
448
+ # Both comments would markup == "" AND line_number == 3
449
+ #
450
+ # There's no way to determine which one is the correct one, but
451
+ # we'll try our best to at least give you one.
452
+ #
453
+ # To screw with you even more, the name of the tag could be
454
+ # outside of a tag on the same line :) But I won't do anything
455
+ # about that (yet?).
456
+ #
457
+ # {% comment
458
+ # %}comment{% endcomment %}
459
+ if tag_markup.empty?
460
+ eol = source.index("\n", start) || source.size
461
+
462
+ # OK here I'm trying one of two things. Either tag_start is on
463
+ # the same line OR tag_start is on a previous line. The line
464
+ # number would be at the end of the whitespace after tag_name.
465
+ unless (tag_start = source.index(tag_name, start)) && tag_start < eol
466
+ tag_start = start
467
+ tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
468
+ tag_start -= @value.tag_name.size
469
+
470
+ # keep track of the error in line_number
471
+ @line_number_offset = source[tag_start...start].count("\n")
472
+ end
473
+ tag_end = tag_start + tag_name.size
474
+ tag_end += 1 while source[tag_end] =~ WHITESPACE
475
+
476
+ # return the real raw content
477
+ @tag_markup = source[tag_start...tag_end]
478
+ return @tag_markup
479
+
480
+ # because line_numbers are not enough to accurately
481
+ # determine the position of the raw markup and because that
482
+ # markup could be present on the same line outside of a Tag. e.g.
483
+ #
484
+ # uhoh {% if uhoh %}
485
+ elsif (match = /#{tag_name} +#{Regexp.escape(tag_markup)}/.match(source, start))
486
+ return @tag_markup = match[0]
487
+ end
488
+
489
+ # find the markup
490
+ markup_start = source.index(tag_markup, start)
491
+ markup_end = markup_start + tag_markup.size
492
+
493
+ # go back until you find the tag_name
494
+ tag_start = markup_start
495
+ tag_start -= 1 while source[tag_start - 1] =~ WHITESPACE
496
+ tag_start -= tag_name.size
497
+
498
+ # keep track of the error in line_number
499
+ @line_number_offset = source[tag_start...markup_start].count("\n")
500
+
501
+ # return the real raw content
502
+ @tag_markup = source[tag_start...markup_end]
503
+ end
504
+
505
+ # Returns the index of the leftmost consecutive whitespace
506
+ # starting from start going backwards.
507
+ #
508
+ # e.g. backtrack_on_whitespace("01 45", 4) would return 2.
509
+ # e.g. backtrack_on_whitespace("{% render %}", 5) would return 2.
510
+ def backtrack_on_whitespace(string, start, whitespace = WHITESPACE)
511
+ i = start
512
+ i -= 1 while string[i - 1] =~ whitespace && i > 0
513
+ i
514
+ end
515
+
516
+ def find_block_delimiter(start_index)
517
+ return nil unless tag? && block?
518
+
519
+ tag_start, tag_end = if inside_liquid_tag?
520
+ [
521
+ /^\s*#{@value.tag_name}\s*/,
522
+ /^\s*end#{@value.tag_name}\s*/
523
+ ]
524
+ else
525
+ [
526
+ /#{Liquid::TagStart}-?\s*#{@value.tag_name}/mi,
527
+ /#{Liquid::TagStart}-?\s*end#{@value.tag_name}\s*-?#{Liquid::TagEnd}/mi
528
+ ]
529
+ end
530
+
531
+ # This little algorithm below find the _correct_ block delimiter
532
+ # (endif, endcase, endcomment) for the current tag. What do I
533
+ # mean by correct? It means the one you'd expect. Making sure
534
+ # that we don't do the naive regex find. Since you can have
535
+ # nested ifs, fors, etc.
536
+ #
537
+ # It works by having a stack, pushing onto the stack when we
538
+ # open a tag of our type_name. And popping when we find a
539
+ # closing tag of our type_name.
540
+ #
541
+ # When the stack is empty, we return the end tag match.
542
+ index = start_index
543
+ stack = []
544
+ stack.push("open")
545
+ loop do
546
+ tag_start_match = tag_start.match(source, index)
547
+ tag_end_match = tag_end.match(source, index)
548
+
549
+ return nil unless tag_end_match
550
+
551
+ # We have found a tag_start and it appeared _before_ the
552
+ # tag_end that we found, thus we push it onto the stack.
553
+ stack.push("open") if tag_start_match && tag_start_match.end(0) < tag_end_match.end(0)
554
+
555
+ # We have found a tag_end, therefore we pop
556
+ stack.pop
557
+
558
+ # Nothing left on the stack, we're done.
559
+ break tag_end_match if stack.empty?
560
+
561
+ # We keep looking from the end of the end tag we just found.
562
+ index = tag_end_match.end(0)
563
+ end
564
+ end
565
+
566
+ def block_end_match
567
+ @block_end_match ||= find_block_delimiter(block_start_end_index)
568
+ end
569
+ end
570
+ end