platformos-check 0.0.1

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