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,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module PlatformosCheck
6
+ class HtmlNode < Node
7
+ extend Forwardable
8
+ include RegexHelpers
9
+ include PositionHelper
10
+ attr_reader :app_file, :parent
11
+
12
+ class << self
13
+ include RegexHelpers
14
+
15
+ def parse(liquid_file)
16
+ placeholder_values = []
17
+ parseable_source = +liquid_file.source.clone
18
+
19
+ # Replace all non-empty liquid tags with ≬{i}######≬ to prevent the HTML
20
+ # parser from freaking out. We transparently replace those placeholders in
21
+ # HtmlNode.
22
+ #
23
+ # We're using base36 to prevent index bleeding on 36^3 tags.
24
+ # `{{x}}` -> `≬#{i}≬` would properly be transformed for 46656 tags in a single file.
25
+ # Should be enough.
26
+ #
27
+ # The base10 alternative would have overflowed at 1000 (`{{x}}` -> `≬1000≬`) which seemed more likely.
28
+ #
29
+ # Didn't go with base64 because of the `=` character that would have messed with HTML parsing.
30
+ #
31
+ # (Note, we're also maintaining newline characters in there so
32
+ # that line numbers match the source...)
33
+ matches(parseable_source, LIQUID_TAG_OR_VARIABLE).each do |m|
34
+ value = m[0]
35
+ next unless value.size > 4 # skip empty tags/variables {%%} and {{}}
36
+
37
+ placeholder_values.push(value)
38
+ key = (placeholder_values.size - 1).to_s(36)
39
+
40
+ # Doing shenanigans so that line numbers match... Ugh.
41
+ keyed_placeholder = parseable_source[m.begin(0)...m.end(0)]
42
+
43
+ # First and last chars are ≬
44
+ keyed_placeholder[0] = "≬"
45
+ keyed_placeholder[-1] = "≬"
46
+
47
+ # Non newline characters are #
48
+ keyed_placeholder.gsub!(/[^\n≬]/, '#')
49
+
50
+ # First few # are replaced by the base10 ID of the tag
51
+ i = -1
52
+ keyed_placeholder.gsub!('#') do
53
+ i += 1
54
+ if i > key.size - 1
55
+ '#'
56
+ else
57
+ key[i]
58
+ end
59
+ end
60
+
61
+ # Replace source by placeholder
62
+ parseable_source[m.begin(0)...m.end(0)] = keyed_placeholder
63
+ end
64
+
65
+ new(
66
+ Nokogiri::HTML5.fragment(parseable_source, max_tree_depth: 400, max_attributes: 400),
67
+ liquid_file,
68
+ placeholder_values,
69
+ parseable_source
70
+ )
71
+ end
72
+ end
73
+
74
+ def initialize(value, app_file, placeholder_values, parseable_source, parent = nil)
75
+ @value = value
76
+ @app_file = app_file
77
+ @placeholder_values = placeholder_values
78
+ @parseable_source = parseable_source
79
+ @parent = parent
80
+ end
81
+
82
+ # @value is not forwarded because we _need_ to replace the
83
+ # placeholders for the HtmlNode to make sense.
84
+ def value
85
+ if literal?
86
+ content
87
+ else
88
+ markup
89
+ end
90
+ end
91
+
92
+ def children
93
+ @children ||= @value
94
+ .children
95
+ .map { |child| HtmlNode.new(child, app_file, @placeholder_values, @parseable_source, self) }
96
+ end
97
+
98
+ def markup
99
+ @markup ||= replace_placeholders(parseable_markup)
100
+ end
101
+
102
+ def line_number
103
+ @value.line
104
+ end
105
+
106
+ def start_index
107
+ position.start_index
108
+ end
109
+
110
+ def end_index
111
+ position.end_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_row
123
+ position.end_row
124
+ end
125
+
126
+ def end_column
127
+ position.end_column
128
+ end
129
+
130
+ def literal?
131
+ @value.name == "text"
132
+ end
133
+
134
+ def element?
135
+ @value.element?
136
+ end
137
+
138
+ def attributes
139
+ @attributes ||= @value.attributes
140
+ .map { |k, v| [replace_placeholders(k), replace_placeholders(v.value)] }
141
+ .to_h
142
+ end
143
+
144
+ def parseable_markup
145
+ return @parseable_source if @value.name == "#document-fragment"
146
+ return @value.to_str if @value.comment?
147
+ return @value.content if literal?
148
+
149
+ start_index = from_row_column_to_index(@parseable_source, line_number - 1, 0)
150
+ @parseable_source
151
+ .match(/<\s*#{name}[^>]*>/im, start_index)[0]
152
+ rescue NoMethodError
153
+ # Don't know what's up with the following issue. Don't think
154
+ # null check is correct approach. This should give us more info.
155
+ # https://github.com/Platform-OS/platformos-lsp/issues/528
156
+ PlatformosCheck.bug(<<~MSG)
157
+ Can't find a parseable tag of name #{name} inside the parseable HTML.
158
+
159
+ Tag name:
160
+ #{@value.name.inspect}
161
+
162
+ File:
163
+ #{@app_file.relative_path}
164
+
165
+ Line number:
166
+ #{line_number}
167
+
168
+ Excerpt:
169
+ ```
170
+ #{@app_file.source.lines[line_number - 1...line_number + 5].join("")}
171
+ ```
172
+
173
+ Parseable Excerpt:
174
+ ```
175
+ #{@parseable_source.lines[line_number - 1...line_number + 5].join("")}
176
+ ```
177
+ MSG
178
+ end
179
+
180
+ def content
181
+ @content ||= replace_placeholders(@value.content)
182
+ end
183
+
184
+ def name
185
+ if @value.name == "#document-fragment"
186
+ "document"
187
+ else
188
+ @value.name
189
+ end
190
+ end
191
+
192
+ private
193
+
194
+ def position
195
+ @position ||= Position.new(
196
+ markup,
197
+ app_file.source,
198
+ line_number_1_indexed: line_number
199
+ )
200
+ end
201
+
202
+ def replace_placeholders(string)
203
+ # Replace all ≬{i}####≬ with the actual content.
204
+ string.gsub(HTML_LIQUID_PLACEHOLDER) do |match|
205
+ key = /[0-9a-z]+/.match(match.delete("\n"))[0]
206
+ @placeholder_values[key.to_i(36)]
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "forwardable"
5
+
6
+ module PlatformosCheck
7
+ class HtmlVisitor
8
+ attr_reader :checks
9
+
10
+ def initialize(checks)
11
+ @checks = checks
12
+ end
13
+
14
+ def visit_liquid_file(liquid_file)
15
+ visit(HtmlNode.parse(liquid_file))
16
+ rescue ArgumentError => e
17
+ call_checks(:on_parse_error, e, liquid_file)
18
+ end
19
+
20
+ private
21
+
22
+ def visit(node)
23
+ call_checks(:on_element, node) if node.element?
24
+ call_checks(:"on_#{node.name}", node)
25
+ node.children.each { |child| visit(child) }
26
+ return if node.literal?
27
+
28
+ call_checks(:"after_#{node.name}", node)
29
+ call_checks(:after_element, node) if node.element?
30
+ end
31
+
32
+ def call_checks(method, *)
33
+ checks.call(method, *)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An in-memory storage is not written to disk. The reasons why you'd
4
+ # want to do that are your own. The idea is to not write to disk
5
+ # something that doesn't need to be there. If you have your platformos_app
6
+ # as a big hash already, leave it like that and save yourself some IO.
7
+ module PlatformosCheck
8
+ class InMemoryStorage < Storage
9
+ attr_reader :root
10
+
11
+ def initialize(files = {}, root = "/dev/null")
12
+ @files = files # Hash<String, String>
13
+ @root = Pathname.new(root)
14
+ end
15
+
16
+ def path(relative_path)
17
+ @root.join(relative_path)
18
+ end
19
+
20
+ def read(relative_path)
21
+ @files[relative_path]
22
+ end
23
+
24
+ def write(relative_path, content)
25
+ @files[relative_path] = content
26
+ end
27
+
28
+ def remove(relative_path)
29
+ @files.delete(relative_path)
30
+ end
31
+
32
+ def mkdir(relative_path)
33
+ @files[relative_path] = nil
34
+ reset_memoizers
35
+ end
36
+
37
+ # TODO: Fix corrector
38
+ # def rename(old_path, new_path)
39
+ # old_path += '/' if old_path[-1] != '/'
40
+ # new_path += '/' if new_path[-1] != '/'
41
+ # @files.transform_keys! { |k| k.sub(/\A#{old_path}/, new_path) }
42
+ #
43
+ # reset_memoizers
44
+ # end
45
+
46
+ def files
47
+ @files.keys
48
+ end
49
+
50
+ def directories
51
+ @directories ||= @files
52
+ .keys
53
+ .flat_map { |relative_path| Pathname.new(relative_path).ascend.to_a }
54
+ .map(&:to_s)
55
+ .uniq
56
+ end
57
+
58
+ def relative_path(absolute_path)
59
+ Pathname.new(absolute_path).relative_path_from(@root).to_s
60
+ end
61
+
62
+ private
63
+
64
+ def reset_memoizers
65
+ @directories = nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module PlatformosCheck
6
+ class JsonFile < AppFile
7
+ def initialize(relative_path, storage)
8
+ super
9
+ @loaded = false
10
+ @content = nil
11
+ @parser_error = nil
12
+ end
13
+
14
+ def content
15
+ load!
16
+ @content
17
+ end
18
+
19
+ def parse_error
20
+ load!
21
+ @parser_error
22
+ end
23
+
24
+ def update_contents(new_content = {})
25
+ raise ArgumentError if new_content.is_a?(String)
26
+
27
+ @content = new_content
28
+ end
29
+
30
+ def write
31
+ pretty = JSON.pretty_generate(@content)
32
+ return unless source.rstrip != pretty.rstrip
33
+
34
+ # Most editors add a trailing \n at the end of files. Here we
35
+ # try to maintain the convention.
36
+ eof = source.end_with?("\n") ? "\n" : ""
37
+ @storage.write(@relative_path, pretty.gsub("\n", @eol) + eof)
38
+ @source = pretty
39
+ end
40
+
41
+ def json?
42
+ true
43
+ end
44
+
45
+ private
46
+
47
+ def load!
48
+ return if @loaded
49
+
50
+ @content = JSON.parse(source)
51
+ rescue JSON::ParserError => e
52
+ @parser_error = e
53
+ ensure
54
+ @loaded = true
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ class JsonHelper
5
+ # Deeply sets a value in a hash. Accepts both arrays and strings for path.
6
+ def self.set(hash, path, value)
7
+ path = path.split('.') if path.is_a?(String)
8
+ path.each_with_index.reduce(hash) do |pointer, (token, index)|
9
+ if index == path.size - 1
10
+ pointer[token] = value
11
+ elsif !pointer.key?(token) || !pointer[token].is_a?(Hash)
12
+ pointer[token] = {}
13
+ end
14
+ pointer[token]
15
+ end
16
+ hash
17
+ end
18
+
19
+ # Deeply delete a key from a hash
20
+ def self.delete(hash, path)
21
+ path = path.split('.') if path.is_a?(String)
22
+ path.each_with_index.reduce(hash) do |pointer, (token, index)|
23
+ break pointer.delete(token) if index == path.size - 1
24
+
25
+ pointer[token]
26
+ end
27
+ end
28
+
29
+ # Deeply add key/values inside a hash.
30
+ #
31
+ # Handles arrays by adding the key/value to all hashes inside the array.
32
+ #
33
+ # Specially handles objects that have the "id" key like this:
34
+ #
35
+ # e.g.
36
+ #
37
+ # json = {
38
+ # "deep" => [
39
+ # { "id" => "hi" },
40
+ # { "id" => "oh" },
41
+ # ],
42
+ # }
43
+ # assert_equal(
44
+ # {
45
+ # "deep" => [
46
+ # { "id" => "hi", "ho" => "ho" },
47
+ # { "id" => "oh" },
48
+ # ],
49
+ # },
50
+ # JsonHelper.json_corrector(json, "deep.hi.ho", "ho")
51
+ # )
52
+ def self.json_corrector(json, path, value)
53
+ return json unless json.is_a?(Hash)
54
+
55
+ path = path.split('.') if path.is_a?(String)
56
+ path.each_with_index.reduce(json) do |pointer, (token, index)|
57
+ case pointer
58
+ when Array
59
+ pointer.each do |item|
60
+ json_corrector(item, path.drop(1), value)
61
+ end
62
+
63
+ when Hash
64
+ break pointer[token] = value if index == path.size - 1
65
+
66
+ pointer[token] = {} unless pointer.key?(token) || pointer.key?("id")
67
+ pointer[token].nil? && pointer["id"] == token ? pointer : pointer[token]
68
+ end
69
+ end
70
+ json
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ module JsonHelpers
5
+ def format_json_parse_error(error)
6
+ message = error.message[/\d+: (.+)$/, 1] || 'Invalid syntax'
7
+ "#{message} in JSON"
8
+ end
9
+
10
+ def pretty_json(hash, start_level: 1, indent: " ")
11
+ start_indent = indent * start_level
12
+
13
+ <<~JSON
14
+
15
+ #{start_indent}#{JSON.pretty_generate(
16
+ hash,
17
+ indent:,
18
+ array_nl: "\n#{start_indent}",
19
+ object_nl: "\n#{start_indent}"
20
+ )}
21
+ JSON
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PlatformosCheck
6
+ class JsonPrinter
7
+ def initialize(out_stream = STDOUT)
8
+ @out = out_stream
9
+ end
10
+
11
+ def print(offenses)
12
+ json = offenses_by_path(offenses)
13
+ @out.puts JSON.dump(json)
14
+ end
15
+
16
+ def offenses_by_path(offenses)
17
+ offenses
18
+ .map(&:to_h)
19
+ .group_by { |offense| offense[:path] }
20
+ .map do |(path, path_offenses)|
21
+ {
22
+ path:,
23
+ offenses: path_offenses.map { |offense| offense.filter { |k, _v| k != :path } },
24
+ errorCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:error] },
25
+ suggestionCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:suggestion] },
26
+ styleCount: path_offenses.count { |offense| offense[:severity] == Check::SEVERITY_VALUES[:style] }
27
+ }
28
+ end
29
+ .sort_by { |o| o[:path] || Pathname.new('') }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ # This class exists as a bridge (or boundary) between our handlers and the outside world.
6
+ #
7
+ # It is concerned with all the Language Server Protocol constructs. i.e.
8
+ #
9
+ # - sending Hash messages as JSON
10
+ # - reading JSON messages as Hashes
11
+ # - preparing, sending and resolving requests
12
+ # - preparing and sending responses
13
+ # - preparing and sending notifications
14
+ # - preparing and sending progress notifications
15
+ #
16
+ # But it _not_ concerned by _how_ those messages are sent to the
17
+ # outside world. That's the job of the messenger.
18
+ #
19
+ # This enables us to have all the language server protocol logic
20
+ # in here living independently of how we communicate with the
21
+ # client (STDIO or websocket)
22
+ module PlatformosCheck
23
+ module LanguageServer
24
+ class Bridge
25
+ attr_writer :supports_work_done_progress
26
+
27
+ def initialize(messenger)
28
+ # The messenger is responsible for IO.
29
+ # Could be STDIO or WebSockets or Mock.
30
+ @messenger = messenger
31
+
32
+ # Whether the client supports work done progress notifications
33
+ @supports_work_done_progress = false
34
+
35
+ @work_done_progress_mutex = Mutex.new
36
+ @work_done_progress_token = 1
37
+ end
38
+
39
+ def log(message)
40
+ @messenger.log(message)
41
+ end
42
+
43
+ def read_message
44
+ message_body = @messenger.read_message
45
+ message_json = JSON.parse(message_body, symbolize_names: true)
46
+ @messenger.log(JSON.pretty_generate(message_json)) if PlatformosCheck.debug?
47
+ message_json
48
+ end
49
+
50
+ def send_message(message_hash)
51
+ message_hash[:jsonrpc] = '2.0'
52
+ message_body = JSON.dump(message_hash)
53
+ @messenger.log(JSON.pretty_generate(message_hash)) if PlatformosCheck.debug?
54
+ @messenger.send_message(message_body)
55
+ end
56
+
57
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#requestMessage
58
+ def send_request(method, params = nil)
59
+ channel = Channel.create
60
+ message = { id: channel.id }
61
+ message[:method] = method
62
+ message[:params] = params if params
63
+ send_message(message)
64
+ channel.pop
65
+ ensure
66
+ channel.close
67
+ end
68
+
69
+ def receive_response(id, result)
70
+ Channel.by_id(id) << result
71
+ end
72
+
73
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage
74
+ def send_response(id, result = nil, error = nil)
75
+ message = { id: }
76
+ if error
77
+ message[:error] = error
78
+ else
79
+ message[:result] = result
80
+ end
81
+ send_message(message)
82
+ end
83
+
84
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
85
+ def send_internal_error(id, e)
86
+ # For a reason I can't comprehend, sometimes
87
+ # e.full_message _hangs_ and brings your CPU to 100%.
88
+ # It's wrapped in here because it prints anyway...
89
+ # This shit is weird, yo.
90
+ Timeout.timeout(1) do
91
+ warn e.full_message
92
+ end
93
+ ensure
94
+ send_response(id, nil, {
95
+ code: ErrorCodes::INTERNAL_ERROR,
96
+ message: "A platformos-check-language-server has occured, inspect OUTPUT logs for details."
97
+ })
98
+ end
99
+
100
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#notificationMessage
101
+ def send_notification(method, params)
102
+ message = { method: }
103
+ message[:params] = params
104
+ send_message(message)
105
+ end
106
+
107
+ # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
108
+ def send_progress(token, value)
109
+ send_notification("$/progress", token:, value:)
110
+ end
111
+
112
+ def supports_work_done_progress?
113
+ @supports_work_done_progress
114
+ end
115
+
116
+ def send_create_work_done_progress_request
117
+ # This isn't necessary, but it kind of is to make it obvious
118
+ # that this variable is not thread safe. Don't try to refactor
119
+ # this with @work_done_progress_token because you're going to
120
+ # have a hard time.
121
+ token = @work_done_progress_mutex.synchronize do
122
+ @work_done_progress_token += 1
123
+ end
124
+
125
+ return token unless supports_work_done_progress?
126
+
127
+ # We're going to wait for a response here...
128
+ send_request("window/workDoneProgress/create", {
129
+ token:
130
+ })
131
+
132
+ token
133
+ end
134
+
135
+ def send_work_done_progress_begin(token, title)
136
+ return unless supports_work_done_progress?
137
+
138
+ send_progress(token, {
139
+ kind: 'begin',
140
+ title:,
141
+ cancellable: false,
142
+ percentage: 0
143
+ })
144
+ end
145
+
146
+ def send_work_done_progress_report(token, message, percentage)
147
+ return unless supports_work_done_progress?
148
+
149
+ send_progress(token, {
150
+ kind: 'report',
151
+ message:,
152
+ cancellable: false,
153
+ percentage:
154
+ })
155
+ end
156
+
157
+ def send_work_done_progress_end(token, message)
158
+ return unless supports_work_done_progress?
159
+
160
+ send_progress(token, {
161
+ kind: 'end',
162
+ message:
163
+ })
164
+ end
165
+ end
166
+ end
167
+ end