mt-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Linter
5
+ module LinterDocTags
6
+ private
7
+
8
+ def emit_doc_tag_warnings(source_file)
9
+ return if @source_lines.empty?
10
+
11
+ each_doc_comment_definition(source_file) do |declaration, doc_lines|
12
+ parsed = parse_doc_tag_block(doc_lines)
13
+
14
+ parsed[:errors].each do |error|
15
+ @warnings << Warning.new(
16
+ path: @path,
17
+ line: error[:line],
18
+ column: error[:column],
19
+ length: error[:length],
20
+ code: "doc-tag",
21
+ message: error[:message],
22
+ severity: :hint,
23
+ )
24
+ end
25
+
26
+ validate_doc_tags_for_declaration(declaration, parsed[:tags])
27
+ end
28
+ end
29
+ def each_doc_comment_definition(source_file)
30
+ declarations = source_file.declarations.flat_map do |declaration|
31
+ case declaration
32
+ when AST::ExtendingBlock
33
+ declaration.methods
34
+ else
35
+ declaration
36
+ end
37
+ end
38
+
39
+ declarations.each do |declaration|
40
+ docs = collect_doc_comment_block_for_line(declaration.line)
41
+ next if docs.nil? || docs.empty?
42
+
43
+ yield declaration, docs
44
+ end
45
+ end
46
+
47
+ def collect_doc_comment_block_for_line(line)
48
+ index = line.to_i - 2
49
+ return nil if index.negative?
50
+
51
+ docs = []
52
+ while index >= 0
53
+ stripped = @source_lines[index].to_s.strip
54
+ break if stripped.empty?
55
+ break unless stripped.start_with?('##')
56
+
57
+ docs << {
58
+ line: index + 1,
59
+ text: stripped.sub(/\A##\s?/, ''),
60
+ }
61
+ index -= 1
62
+ end
63
+
64
+ return nil if docs.empty?
65
+
66
+ docs.reverse
67
+ end
68
+ def parse_doc_tag_block(lines)
69
+ tags = {
70
+ params: [],
71
+ returns: nil,
72
+ throws: [],
73
+ see: [],
74
+ }
75
+ errors = []
76
+
77
+ lines.each do |entry|
78
+ text = entry[:text].to_s
79
+ match = text.match(/\A\s*@([A-Za-z_][A-Za-z0-9_-]*)(?:\s+(.*))?\z/)
80
+ next unless match
81
+
82
+ tag = match[1].to_s.downcase
83
+ payload = match[2].to_s.strip
84
+ column = doc_tag_column(entry[:line])
85
+
86
+ case tag
87
+ when 'param'
88
+ if payload.empty?
89
+ errors << {
90
+ line: entry[:line],
91
+ column: column,
92
+ length: 6,
93
+ message: 'doc tag @param requires a parameter name',
94
+ }
95
+ next
96
+ end
97
+
98
+ name_match = payload.match(/\A([A-Za-z_][A-Za-z0-9_]*)(?:\s+(.*))?\z/)
99
+ unless name_match
100
+ errors << {
101
+ line: entry[:line],
102
+ column: column,
103
+ length: 6,
104
+ message: 'doc tag @param has an invalid parameter name',
105
+ }
106
+ next
107
+ end
108
+
109
+ tags[:params] << {
110
+ name: name_match[1],
111
+ text: name_match[2].to_s.strip,
112
+ line: entry[:line],
113
+ column: column,
114
+ }
115
+ when 'return', 'returns'
116
+ tags[:returns] = {
117
+ text: payload,
118
+ line: entry[:line],
119
+ column: column,
120
+ }
121
+ when 'throws', 'throw'
122
+ tags[:throws] << {
123
+ text: payload,
124
+ line: entry[:line],
125
+ column: column,
126
+ }
127
+ when 'see'
128
+ tags[:see] << {
129
+ text: payload,
130
+ line: entry[:line],
131
+ column: column,
132
+ }
133
+ else
134
+ errors << {
135
+ line: entry[:line],
136
+ column: column,
137
+ length: tag.length + 1,
138
+ message: "unknown doc tag @#{tag}",
139
+ }
140
+ end
141
+ end
142
+
143
+ {
144
+ tags: tags,
145
+ errors: errors,
146
+ }
147
+ end
148
+ def validate_doc_tags_for_declaration(declaration, tags)
149
+ callable = declaration.respond_to?(:params) && declaration.respond_to?(:return_type)
150
+ param_tags = Array(tags[:params])
151
+ return_tag = tags[:returns]
152
+
153
+ unless callable
154
+ ([*param_tags, return_tag].compact).each do |tag_entry|
155
+ @warnings << Warning.new(
156
+ path: @path,
157
+ line: tag_entry[:line],
158
+ column: tag_entry[:column],
159
+ length: 1,
160
+ code: 'doc-tag',
161
+ message: 'callable doc tags are only valid on function and method declarations',
162
+ severity: :hint,
163
+ )
164
+ end
165
+ return
166
+ end
167
+
168
+ params = Array(declaration.params)
169
+ param_names = params.map(&:name)
170
+ param_name_set = param_names.to_set
171
+
172
+ param_tags.each do |tag_entry|
173
+ next if param_name_set.include?(tag_entry[:name])
174
+
175
+ @warnings << Warning.new(
176
+ path: @path,
177
+ line: tag_entry[:line],
178
+ column: tag_entry[:column],
179
+ length: tag_entry[:name].length,
180
+ code: 'doc-tag',
181
+ message: "doc tag @param '#{tag_entry[:name]}' does not match any parameter in '#{declaration.name}'",
182
+ severity: :hint,
183
+ symbol_name: tag_entry[:name],
184
+ )
185
+ end
186
+
187
+ return unless return_tag
188
+
189
+ return_type_text = declaration.return_type.to_s
190
+ return unless return_type_text == 'void'
191
+
192
+ @warnings << Warning.new(
193
+ path: @path,
194
+ line: return_tag[:line],
195
+ column: return_tag[:column],
196
+ length: 7,
197
+ code: 'doc-tag',
198
+ message: "doc tag @return is stale for '#{declaration.name}' because it returns void",
199
+ severity: :hint,
200
+ )
201
+ end
202
+
203
+ def doc_tag_column(line)
204
+ text = @source_lines[line.to_i - 1].to_s
205
+ at_index = text.index('@')
206
+ return 1 if at_index.nil?
207
+
208
+ at_index + 1
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Linter
5
+ FixEdit = Data.define(:start_line, :start_char, :end_line, :end_char, :new_text)
6
+
7
+ module FixEngine
8
+ module_function
9
+
10
+ def edits_for_rule(code, lines, warning)
11
+ case code
12
+ when "prefer-let" then prefer_let_edits(lines, warning)
13
+ when "redundant-ignored-match-binding" then redundant_ignored_match_binding_edits(lines, warning)
14
+ when "prefer-let-else" then prefer_let_else_edits(lines, warning)
15
+ when "prefer-var-else" then prefer_var_else_edits(lines, warning)
16
+ when "redundant-bool-compare" then redundant_bool_compare_edits(lines, warning)
17
+ when "redundant-cast" then redundant_cast_edits(lines, warning)
18
+ when "redundant-else" then redundant_else_edits(lines, warning)
19
+ when "redundant-return" then redundant_return_edits(lines, warning)
20
+ when "redundant-type-annotation" then redundant_type_annotation_edits(lines, warning)
21
+ when "unused-import" then unused_import_edits(lines, warning)
22
+ when "trailing-list-comma" then trailing_list_comma_edits(lines, warning)
23
+ else []
24
+ end
25
+ end
26
+
27
+ def apply_fix_edits(lines, edits)
28
+ edits.sort_by { |e| [-e.start_line, -e.start_char] }.reverse_each do |edit|
29
+ if edit.start_line == edit.end_line
30
+ line = lines[edit.start_line]
31
+ next unless line
32
+
33
+ lines[edit.start_line] = +"#{line[0...edit.start_char]}#{edit.new_text}#{line[edit.end_char..]}"
34
+ else
35
+ prefix = (l = lines[edit.start_line]) ? l[0...edit.start_char] : ""
36
+ suffix = (l = lines[edit.end_line]) ? l[edit.end_char..] : ""
37
+ lines[edit.start_line..edit.end_line] = ["#{prefix}#{edit.new_text}#{suffix}"]
38
+ end
39
+ end
40
+ lines
41
+ end
42
+
43
+ def edits_to_lsp_text_edits(edits, uri)
44
+ edits.map do |edit|
45
+ {
46
+ range: {
47
+ start: { line: edit.start_line, character: edit.start_char },
48
+ end: { line: edit.end_line, character: edit.end_char },
49
+ },
50
+ newText: edit.new_text,
51
+ }
52
+ end
53
+ end
54
+
55
+ # ── per-rule edit generators ──────────────────────────────────────────
56
+
57
+ def prefer_let_edits(lines, warning)
58
+ return [] unless warning.line
59
+
60
+ line_idx = warning.line - 1
61
+ original = lines[line_idx]
62
+ return [] unless original&.match?(/\bvar\b/)
63
+
64
+ new_line = original.sub(/\bvar\b/, "let")
65
+ [FixEdit.new(start_line: line_idx, start_char: 0, end_line: line_idx + 1, end_char: 0, new_text: new_line)]
66
+ end
67
+
68
+ def redundant_ignored_match_binding_edits(lines, warning)
69
+ return [] unless warning.line && warning.column
70
+
71
+ line_idx = warning.line - 1
72
+ line = lines[line_idx]
73
+ return [] unless line
74
+
75
+ span = Linter.redundant_ignored_match_binding_span(line, column: warning.column)
76
+ return [] unless span
77
+
78
+ [FixEdit.new(start_line: line_idx, start_char: span[:start_char], end_line: line_idx, end_char: span[:end_char], new_text: "")]
79
+ end
80
+
81
+ def prefer_let_else_edits(lines, warning)
82
+ return [] unless warning.line
83
+
84
+ fix = Linter.build_prefer_let_else_fix(lines, warning.line - 1, symbol_name: warning.symbol_name)
85
+ return [] unless fix
86
+
87
+ [FixEdit.new(start_line: fix[:start_line_idx], start_char: 0, end_line: fix[:end_line_idx] + 1, end_char: 0, new_text: fix[:new_text])]
88
+ end
89
+
90
+ def prefer_var_else_edits(lines, warning)
91
+ prefer_let_else_edits(lines, warning)
92
+ end
93
+
94
+ def redundant_bool_compare_edits(lines, warning)
95
+ return [] unless warning.line && warning.column && warning.length
96
+
97
+ line_idx = warning.line - 1
98
+ line = lines[line_idx]
99
+ return [] unless line
100
+
101
+ start_char = warning.column - 1
102
+ end_char = start_char + warning.length
103
+ return [] if start_char.negative? || end_char > line.length
104
+
105
+ expr_text = line[start_char...end_char]
106
+ replacement = Linter.redundant_bool_compare_replacement(expr_text)
107
+ return [] unless replacement
108
+
109
+ [FixEdit.new(start_line: line_idx, start_char:, end_line: line_idx, end_char:, new_text: replacement)]
110
+ end
111
+
112
+ def redundant_else_edits(lines, warning)
113
+ return [] unless warning.line
114
+
115
+ diag_idx = warning.line - 1
116
+ return [] if diag_idx.negative?
117
+
118
+ if lines[diag_idx]&.match?(/\A\s*else:\s*\z/)
119
+ else_idx = diag_idx
120
+ first_body_idx = else_idx + 1
121
+ else
122
+ first_body_idx = diag_idx
123
+ return [] if first_body_idx < 1
124
+
125
+ else_idx = (0...first_body_idx).to_a.reverse.find { |i| lines[i]&.match?(/\A\s*else:\s*\z/) }
126
+ end
127
+ return [] unless else_idx
128
+ return [] if first_body_idx >= lines.length
129
+
130
+ else_indent = lines[else_idx].match(/\A(\s*)/)[1]
131
+ body_indent = "#{else_indent} "
132
+
133
+ body_end_idx = first_body_idx
134
+ (first_body_idx...lines.length).each do |i|
135
+ l = lines[i]
136
+ if l.chomp.empty? || l.start_with?(body_indent)
137
+ body_end_idx = i
138
+ else
139
+ break
140
+ end
141
+ end
142
+
143
+ new_body = lines[first_body_idx..body_end_idx].map { |l| l.sub(/\A /, "") }.join
144
+ [FixEdit.new(start_line: else_idx, start_char: 0, end_line: body_end_idx + 1, end_char: 0, new_text: new_body)]
145
+ end
146
+
147
+ def redundant_return_edits(lines, warning)
148
+ return [] unless warning.line
149
+
150
+ line_idx = warning.line - 1
151
+ return [] unless lines[line_idx]&.match?(/\A\s*return\s*\z/)
152
+
153
+ [FixEdit.new(start_line: line_idx, start_char: 0, end_line: line_idx + 1, end_char: 0, new_text: "")]
154
+ end
155
+
156
+ def unused_import_edits(lines, warning)
157
+ return [] unless warning.line
158
+
159
+ line_idx = warning.line - 1
160
+ return [] unless lines[line_idx]&.match?(/\A\s*import\b/)
161
+
162
+ [FixEdit.new(start_line: line_idx, start_char: 0, end_line: line_idx + 1, end_char: 0, new_text: "")]
163
+ end
164
+
165
+ def trailing_list_comma_edits(lines, warning)
166
+ return [] unless warning.line && warning.column
167
+
168
+ line_idx = warning.line - 1
169
+ line = lines[line_idx]
170
+ return [] unless line
171
+
172
+ char_idx = warning.column - 1
173
+ return [] if char_idx.negative? || char_idx >= line.length
174
+ return [] unless line[char_idx] == ","
175
+
176
+ [FixEdit.new(start_line: line_idx, start_char: char_idx, end_line: line_idx, end_char: char_idx + 1, new_text: "")]
177
+ end
178
+
179
+ def redundant_type_annotation_edits(lines, warning)
180
+ return [] unless warning.line
181
+
182
+ line_idx = warning.line - 1
183
+ line = lines[line_idx]
184
+ return [] unless line
185
+
186
+ name_len = warning.length
187
+ col = warning.column - 1
188
+ return [] if col.negative? || col + name_len > line.length
189
+
190
+ after_name = col + name_len
191
+ rest = line[after_name..]
192
+ return [] unless rest
193
+
194
+ type_match = rest.match(/\s*:\s*\S+/)
195
+ return [] unless type_match
196
+
197
+ new_line = +"#{line[0...after_name]}#{rest[type_match.end(0)..]}"
198
+ [FixEdit.new(start_line: line_idx, start_char: 0, end_line: line_idx + 1, end_char: 0, new_text: new_line)]
199
+ end
200
+
201
+ def redundant_cast_edits(lines, warning)
202
+ return [] unless warning.line && warning.column
203
+
204
+ line_idx = warning.line - 1
205
+ line = lines[line_idx]
206
+ return [] unless line
207
+
208
+ start = warning.column - 1
209
+ return [] unless start >= 0 && start < line.length
210
+
211
+ # The warning column points at the cast target type. Delete the
212
+ # `TargetType<-` prefix, keeping the inner expression as-is. This
213
+ # covers both bare (`ulong<-x`) and parenthesized (`uint<-(a - b)`)
214
+ # forms; type refs never contain `<`, so the first `<-` at/after the
215
+ # column is the cast arrow.
216
+ arrow = line.index("<-", start)
217
+ return [] unless arrow
218
+
219
+ type_ref = line[start...arrow]
220
+ return [] unless type_ref.match?(/\A[A-Za-z_][A-Za-z0-9_\[\]\.,\s@]*\z/)
221
+
222
+ # When the cast is inside an `unsafe:` expression, also remove the
223
+ # `unsafe: ` wrapper. Look backward from the cast for `unsafe:`.
224
+ new_start = start
225
+ unsafe_keyword = line.rindex("unsafe:", new_start)
226
+ if unsafe_keyword
227
+ prefix = line[unsafe_keyword...new_start]
228
+ if prefix.match?(/\Aunsafe:\s*\z/)
229
+ new_start = unsafe_keyword
230
+ end
231
+ end
232
+
233
+ [FixEdit.new(start_line: line_idx, start_char: new_start, end_line: line_idx, end_char: arrow + 2, new_text: "")]
234
+ end
235
+ end
236
+ end
237
+ end