mt-lang 0.3.9 → 0.3.11

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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/docs/language-design.md +1 -2
  4. data/docs/language-manual.md +12 -1
  5. data/lib/milk_tea/base.rb +1 -2
  6. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +0 -2
  7. data/lib/milk_tea/bindings/bindgen/declaration.rb +0 -2
  8. data/lib/milk_tea/bindings/bindgen/emitter.rb +0 -2
  9. data/lib/milk_tea/bindings/bindgen/overrides.rb +0 -2
  10. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +51 -53
  11. data/lib/milk_tea/bindings/imported_bindings/generator.rb +7 -9
  12. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +0 -2
  13. data/lib/milk_tea/bindings/imported_bindings/naming.rb +6 -8
  14. data/lib/milk_tea/bindings/imported_bindings.rb +0 -2
  15. data/lib/milk_tea/bindings/opengl_registry.rb +3 -7
  16. data/lib/milk_tea/bindings/steamworks.rb +12 -16
  17. data/lib/milk_tea/bindings/upstream_sources.rb +10 -14
  18. data/lib/milk_tea/bindings/vendored_box2d.rb +10 -12
  19. data/lib/milk_tea/bindings/vendored_cjson.rb +6 -8
  20. data/lib/milk_tea/bindings/vendored_flecs.rb +10 -12
  21. data/lib/milk_tea/bindings/vendored_glfw.rb +11 -13
  22. data/lib/milk_tea/bindings/vendored_libuv.rb +11 -13
  23. data/lib/milk_tea/bindings/vendored_pcre2.rb +11 -13
  24. data/lib/milk_tea/bindings/vendored_raylib.rb +6 -10
  25. data/lib/milk_tea/bindings/vendored_sdl3.rb +11 -13
  26. data/lib/milk_tea/bindings/vendored_steamworks.rb +1 -5
  27. data/lib/milk_tea/core/ast.rb +109 -51
  28. data/lib/milk_tea/core/async_runtime_installer.rb +0 -2
  29. data/lib/milk_tea/core/bindings/attribute_binding.rb +1 -3
  30. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +147 -84
  31. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +394 -396
  32. data/lib/milk_tea/core/c_backend/expressions.rb +489 -476
  33. data/lib/milk_tea/core/c_backend/feature_detection.rb +446 -437
  34. data/lib/milk_tea/core/c_backend/format_helpers.rb +0 -2
  35. data/lib/milk_tea/core/c_backend/reachability.rb +339 -341
  36. data/lib/milk_tea/core/c_backend/reinterpret.rb +231 -217
  37. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +531 -526
  38. data/lib/milk_tea/core/c_backend/statements.rb +528 -530
  39. data/lib/milk_tea/core/c_backend/type_collectors.rb +1344 -1346
  40. data/lib/milk_tea/core/c_backend/type_declaration.rb +215 -213
  41. data/lib/milk_tea/core/c_backend/type_system.rb +347 -349
  42. data/lib/milk_tea/core/c_backend.rb +17 -9
  43. data/lib/milk_tea/core/compatibility_helpers.rb +0 -2
  44. data/lib/milk_tea/core/compile_time/const_eval.rb +7 -12
  45. data/lib/milk_tea/core/compile_time.rb +32 -38
  46. data/lib/milk_tea/core/control_flow/builder.rb +3 -5
  47. data/lib/milk_tea/core/control_flow/constant_propagation.rb +14 -17
  48. data/lib/milk_tea/core/control_flow/definite_assignment.rb +0 -1
  49. data/lib/milk_tea/core/ir.rb +10 -10
  50. data/lib/milk_tea/core/lexer/character_classes.rb +5 -7
  51. data/lib/milk_tea/core/lexer/format_strings.rb +7 -9
  52. data/lib/milk_tea/core/lexer/heredocs.rb +22 -22
  53. data/lib/milk_tea/core/lexer/indentation.rb +0 -2
  54. data/lib/milk_tea/core/lexer/numbers.rb +0 -2
  55. data/lib/milk_tea/core/lexer/recovery.rb +0 -2
  56. data/lib/milk_tea/core/lexer/strings.rb +0 -2
  57. data/lib/milk_tea/core/lexer/symbols.rb +0 -2
  58. data/lib/milk_tea/core/lexer/trivia.rb +0 -2
  59. data/lib/milk_tea/core/lexer.rb +0 -2
  60. data/lib/milk_tea/core/lowering/async/analysis.rb +0 -2
  61. data/lib/milk_tea/core/lowering/async/lowering.rb +1264 -1265
  62. data/lib/milk_tea/core/lowering/async/normalization.rb +5 -3
  63. data/lib/milk_tea/core/lowering/async.rb +627 -630
  64. data/lib/milk_tea/core/lowering/block.rb +413 -416
  65. data/lib/milk_tea/core/lowering/calls.rb +1381 -1384
  66. data/lib/milk_tea/core/lowering/declarations.rb +180 -183
  67. data/lib/milk_tea/core/lowering/dyn.rb +48 -50
  68. data/lib/milk_tea/core/lowering/events.rb +983 -985
  69. data/lib/milk_tea/core/lowering/expressions.rb +1513 -1518
  70. data/lib/milk_tea/core/lowering/foreign_cstr.rb +181 -184
  71. data/lib/milk_tea/core/lowering/functions.rb +185 -188
  72. data/lib/milk_tea/core/lowering/loops.rb +1028 -1031
  73. data/lib/milk_tea/core/lowering/proc.rb +363 -366
  74. data/lib/milk_tea/core/lowering/resolve.rb +2133 -2135
  75. data/lib/milk_tea/core/lowering/scans.rb +179 -181
  76. data/lib/milk_tea/core/lowering/str_buffer.rb +102 -105
  77. data/lib/milk_tea/core/lowering/utils.rb +1040 -1044
  78. data/lib/milk_tea/core/lowering.rb +5 -7
  79. data/lib/milk_tea/core/module_binder.rb +0 -2
  80. data/lib/milk_tea/core/module_loader/errors.rb +4 -2
  81. data/lib/milk_tea/core/module_loader.rb +113 -20
  82. data/lib/milk_tea/core/module_path_resolver.rb +0 -2
  83. data/lib/milk_tea/core/module_roots.rb +6 -8
  84. data/lib/milk_tea/core/parser/attributes.rb +0 -2
  85. data/lib/milk_tea/core/parser/blocks.rb +0 -2
  86. data/lib/milk_tea/core/parser/declarations.rb +7 -9
  87. data/lib/milk_tea/core/parser/expressions.rb +15 -10
  88. data/lib/milk_tea/core/parser/recovery.rb +0 -2
  89. data/lib/milk_tea/core/parser/statements.rb +8 -2
  90. data/lib/milk_tea/core/parser/types.rb +12 -14
  91. data/lib/milk_tea/core/parser.rb +0 -2
  92. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +31 -33
  93. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +0 -2
  94. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +0 -2
  95. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +0 -2
  96. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +14 -16
  97. data/lib/milk_tea/core/semantic_analyzer/calls.rb +0 -2
  98. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1 -14
  99. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +18 -21
  100. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +0 -3
  101. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +14 -17
  102. data/lib/milk_tea/core/semantic_analyzer/generics.rb +57 -59
  103. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +0 -2
  104. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +5 -9
  105. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +0 -3
  106. data/lib/milk_tea/core/semantic_analyzer/statements.rb +35 -40
  107. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +10 -12
  108. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +0 -6
  109. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +106 -109
  110. data/lib/milk_tea/core/semantic_analyzer.rb +17 -8
  111. data/lib/milk_tea/core/types/layout.rb +21 -22
  112. data/lib/milk_tea/core/types/predicates.rb +0 -2
  113. data/lib/milk_tea/core/types/registry.rb +16 -18
  114. data/lib/milk_tea/core/types/visitor.rb +0 -4
  115. data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -2
  116. data/lib/milk_tea/dap/protocol.rb +1 -1
  117. data/lib/milk_tea/dap/server/breakpoints.rb +0 -2
  118. data/lib/milk_tea/dap/server/debug_map.rb +0 -2
  119. data/lib/milk_tea/dap/server/handlers.rb +0 -2
  120. data/lib/milk_tea/dap/server/launch.rb +0 -2
  121. data/lib/milk_tea/dap/server/lldb_backend.rb +8 -10
  122. data/lib/milk_tea/dap/server/pause_diagnostics.rb +14 -16
  123. data/lib/milk_tea/dap/server/utilities.rb +0 -2
  124. data/lib/milk_tea/dap/server/wire.rb +0 -2
  125. data/lib/milk_tea/dap/server.rb +0 -2
  126. data/lib/milk_tea/lsp/diagnostics.rb +16 -16
  127. data/lib/milk_tea/lsp/server/call_hierarchy.rb +11 -12
  128. data/lib/milk_tea/lsp/server/code_actions.rb +355 -357
  129. data/lib/milk_tea/lsp/server/code_lens.rb +23 -25
  130. data/lib/milk_tea/lsp/server/completion.rb +873 -875
  131. data/lib/milk_tea/lsp/server/configuration.rb +129 -131
  132. data/lib/milk_tea/lsp/server/debug_info.rb +0 -2
  133. data/lib/milk_tea/lsp/server/definition.rb +564 -565
  134. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +191 -193
  135. data/lib/milk_tea/lsp/server/execute_command.rb +0 -2
  136. data/lib/milk_tea/lsp/server/folding_range.rb +0 -4
  137. data/lib/milk_tea/lsp/server/formatting.rb +478 -480
  138. data/lib/milk_tea/lsp/server/hover.rb +1282 -1284
  139. data/lib/milk_tea/lsp/server/inlay_hints.rb +179 -181
  140. data/lib/milk_tea/lsp/server/lifecycle.rb +192 -194
  141. data/lib/milk_tea/lsp/server/linked_editing_range.rb +0 -2
  142. data/lib/milk_tea/lsp/server/on_type_formatting.rb +0 -2
  143. data/lib/milk_tea/lsp/server/progress.rb +0 -2
  144. data/lib/milk_tea/lsp/server/references.rb +316 -318
  145. data/lib/milk_tea/lsp/server/rename.rb +445 -447
  146. data/lib/milk_tea/lsp/server/selection_range.rb +0 -4
  147. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1376 -1314
  148. data/lib/milk_tea/lsp/server/signature_help.rb +146 -148
  149. data/lib/milk_tea/lsp/server/text_documents.rb +99 -101
  150. data/lib/milk_tea/lsp/server/type_hierarchy.rb +2 -4
  151. data/lib/milk_tea/lsp/server/utilities.rb +397 -399
  152. data/lib/milk_tea/lsp/server.rb +0 -3
  153. data/lib/milk_tea/lsp/workspace/analysis.rb +40 -42
  154. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +4 -10
  155. data/lib/milk_tea/lsp/workspace/utilities.rb +19 -19
  156. data/lib/milk_tea/packages/atomic_write.rb +4 -5
  157. data/lib/milk_tea/packages/dependency_solver.rb +15 -15
  158. data/lib/milk_tea/packages/lock.rb +4 -4
  159. data/lib/milk_tea/packages/manager_cli.rb +1 -1
  160. data/lib/milk_tea/packages/manifest.rb +3 -3
  161. data/lib/milk_tea/packages/registry_store.rb +4 -6
  162. data/lib/milk_tea/packages/services.rb +16 -16
  163. data/lib/milk_tea/packages/source_cache.rb +4 -6
  164. data/lib/milk_tea/packages/source_resolver.rb +44 -44
  165. data/lib/milk_tea/packages/version.rb +8 -8
  166. data/lib/milk_tea/tooling/build.rb +3 -7
  167. data/lib/milk_tea/tooling/build_cache.rb +0 -2
  168. data/lib/milk_tea/tooling/cli.rb +10 -4
  169. data/lib/milk_tea/tooling/debug_info_formatter.rb +31 -33
  170. data/lib/milk_tea/tooling/debug_map.rb +8 -10
  171. data/lib/milk_tea/tooling/docs_app.rb +0 -2
  172. data/lib/milk_tea/tooling/linter/doc_tags.rb +25 -27
  173. data/lib/milk_tea/tooling/linter/fix_engine.rb +87 -24
  174. data/lib/milk_tea/tooling/linter/flow_rules.rb +44 -46
  175. data/lib/milk_tea/tooling/linter/imports_platform.rb +14 -16
  176. data/lib/milk_tea/tooling/linter/release_rules.rb +3 -5
  177. data/lib/milk_tea/tooling/linter/reserved_names.rb +23 -25
  178. data/lib/milk_tea/tooling/linter/rules.rb +87 -89
  179. data/lib/milk_tea/tooling/linter/source_helpers.rb +40 -42
  180. data/lib/milk_tea/tooling/linter/trailing_comma.rb +17 -19
  181. data/lib/milk_tea/tooling/linter/visitors.rb +112 -54
  182. data/lib/milk_tea/tooling/linter.rb +0 -2
  183. data/lib/milk_tea/tooling/project_scaffold.rb +0 -2
  184. data/lib/milk_tea/tooling/run.rb +0 -2
  185. data/lib/milk_tea/tooling/toolchain_cli.rb +0 -2
  186. data/std/cli.mt +13 -32
  187. data/std/color.mt +28 -28
  188. data/std/encoding.mt +4 -4
  189. data/std/graph.mt +1 -2
  190. data/std/hash.mt +14 -14
  191. data/std/http/server.mt +3 -3
  192. data/std/input.mt +1 -1
  193. data/std/json.mt +1 -1
  194. data/std/linear_algebra.mt +6 -6
  195. data/std/net/mux.mt +1 -1
  196. data/std/noise.mt +23 -23
  197. data/std/parse.mt +2 -2
  198. data/std/process.mt +3 -4
  199. data/std/sparse_set.mt +1 -1
  200. data/std/steering.mt +5 -5
  201. data/std/str.mt +1 -1
  202. data/std/tween.mt +4 -4
  203. data/std/utility.mt +3 -3
  204. metadata +2 -2
@@ -6,451 +6,449 @@ module MilkTea
6
6
  FakeWarning = Struct.new(:line, :column, :length, :symbol_name, keyword_init: true)
7
7
 
8
8
  module ServerCodeActions
9
- private
10
-
11
- def handle_code_action(params)
12
- uri = params.dig('textDocument', 'uri')
13
- return [] unless uri
14
-
15
- total_start = monotonic_time
16
- content = @workspace.get_content(uri)
17
- return [] unless content
18
-
19
- only_kinds = params.dig('context', 'only')
20
- want_quickfix = only_kinds.nil? || only_kinds.any? { |k| k == 'quickFix' || k.start_with?('quickFix.') }
21
- want_fixall = !only_kinds.nil? && only_kinds.any? { |k| k == 'source.fixAll' || k == 'source' || k.start_with?('source.') }
22
-
23
- actions = []
24
- lines = content.lines
25
- requested_diagnostics = params.dig('context', 'diagnostics') || []
26
- reserved_primitive_name_fixes = nil
27
-
28
- quickfix_start = monotonic_time
29
-
30
- # ── Per-diagnostic quickfix actions ──────────────────────────────────
31
- if want_quickfix
32
- requested_diagnostics.each do |diag|
33
- code = diag['code']
34
- message = diag['message'].to_s
35
- diag_line = diag.dig('range', 'start', 'line').to_i + 1 # 1-based
36
- diag_start_char = diag.dig('range', 'start', 'character').to_i
37
- diag_end_char = diag.dig('range', 'end', 'character').to_i
38
- source_line = lines[diag_line - 1].to_s
39
-
40
- if message.start_with?('cannot assign ') && !source_line.empty?
41
- expected_type = message[/\bexpected\s+(.+)\z/, 1]&.strip
42
- equal_index = source_line.index('=')
43
- if expected_type && !expected_type.empty? && equal_index
44
- rhs = source_line[(equal_index + 1)..]&.strip
45
- if rhs && !rhs.empty? && !rhs.start_with?("#{expected_type}<-")
46
- indent = source_line[/\A\s*/] || ''
47
- lhs = source_line[0..equal_index].rstrip
48
- simple_value = rhs.match?(/\A(?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|[0-9][0-9_]*(?:\.[0-9][0-9_]*)?|0x[0-9A-Fa-f_]+|0b[01_]+|0o[0-7_]+)\z/)
49
- casted_rhs = simple_value ? "#{expected_type}<-#{rhs}" : "#{expected_type}<-(#{rhs})"
50
- new_line = "#{indent}#{lhs} #{casted_rhs}\n"
51
- actions << {
52
- title: "Cast expression to #{expected_type}",
53
- kind: 'quickFix',
54
- diagnostics: [diag],
55
- edit: {
56
- changes: {
57
- uri => [{
58
- range: {
59
- start: { line: diag_line - 1, character: 0 },
60
- end: { line: diag_line, character: 0 }
61
- },
62
- newText: new_line
63
- }]
9
+ def handle_code_action(params)
10
+ uri = params.dig('textDocument', 'uri')
11
+ return [] unless uri
12
+
13
+ total_start = monotonic_time
14
+ content = @workspace.get_content(uri)
15
+ return [] unless content
16
+
17
+ only_kinds = params.dig('context', 'only')
18
+ want_quickfix = only_kinds.nil? || only_kinds.any? { |k| k == 'quickFix' || k.start_with?('quickFix.') }
19
+ want_fixall = !only_kinds.nil? && only_kinds.any? { |k| k == 'source.fixAll' || k == 'source' || k.start_with?('source.') }
20
+
21
+ actions = []
22
+ lines = content.lines
23
+ requested_diagnostics = params.dig('context', 'diagnostics') || []
24
+ reserved_primitive_name_fixes = nil
25
+
26
+ quickfix_start = monotonic_time
27
+
28
+ # ── Per-diagnostic quickfix actions ──────────────────────────────────
29
+ if want_quickfix
30
+ requested_diagnostics.each do |diag|
31
+ code = diag['code']
32
+ message = diag['message'].to_s
33
+ diag_line = diag.dig('range', 'start', 'line').to_i + 1 # 1-based
34
+ diag_start_char = diag.dig('range', 'start', 'character').to_i
35
+ diag_end_char = diag.dig('range', 'end', 'character').to_i
36
+ source_line = lines[diag_line - 1].to_s
37
+
38
+ if message.start_with?('cannot assign ') && !source_line.empty?
39
+ expected_type = message[/\bexpected\s+(.+)\z/, 1]&.strip
40
+ equal_index = source_line.index('=')
41
+ if expected_type && !expected_type.empty? && equal_index
42
+ rhs = source_line[(equal_index + 1)..]&.strip
43
+ if rhs && !rhs.empty? && !rhs.start_with?("#{expected_type}<-")
44
+ indent = source_line[/\A\s*/] || ''
45
+ lhs = source_line[0..equal_index].rstrip
46
+ simple_value = rhs.match?(/\A(?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|[0-9][0-9_]*(?:\.[0-9][0-9_]*)?|0x[0-9A-Fa-f_]+|0b[01_]+|0o[0-7_]+)\z/)
47
+ casted_rhs = simple_value ? "#{expected_type}<-#{rhs}" : "#{expected_type}<-(#{rhs})"
48
+ new_line = "#{indent}#{lhs} #{casted_rhs}\n"
49
+ actions << {
50
+ title: "Cast expression to #{expected_type}",
51
+ kind: 'quickFix',
52
+ diagnostics: [diag],
53
+ edit: {
54
+ changes: {
55
+ uri => [{
56
+ range: {
57
+ start: { line: diag_line - 1, character: 0 },
58
+ end: { line: diag_line, character: 0 }
59
+ },
60
+ newText: new_line
61
+ }]
62
+ }
64
63
  }
65
64
  }
66
- }
65
+ end
67
66
  end
68
67
  end
69
- end
70
68
 
71
- case code
72
- when 'reserved-primitive-name'
73
- reserved_primitive_name_fixes ||= Linter.collect_reserved_primitive_name_fixes(content, path: uri)
74
- fix = reserved_primitive_name_fixes.find do |candidate|
75
- declaration_site = candidate.sites.first
76
- declaration_site.line == diag_line && declaration_site.column == (diag_start_char + 1)
77
- end
78
- next unless fix
79
-
80
- edits = fix.sites.uniq { |site| [site.line, site.column] }.sort_by { |site| [site.line, site.column] }.map do |site|
81
- {
82
- range: {
83
- start: { line: site.line - 1, character: site.column - 1 },
84
- end: { line: site.line - 1, character: site.column - 1 + site.length }
85
- },
86
- newText: fix.replacement_name
87
- }
88
- end
69
+ case code
70
+ when 'reserved-primitive-name'
71
+ reserved_primitive_name_fixes ||= Linter.collect_reserved_primitive_name_fixes(content, path: uri)
72
+ fix = reserved_primitive_name_fixes.find do |candidate|
73
+ declaration_site = candidate.sites.first
74
+ declaration_site.line == diag_line && declaration_site.column == (diag_start_char + 1)
75
+ end
76
+ next unless fix
77
+
78
+ edits = fix.sites.uniq { |site| [site.line, site.column] }.sort_by { |site| [site.line, site.column] }.map do |site|
79
+ {
80
+ range: {
81
+ start: { line: site.line - 1, character: site.column - 1 },
82
+ end: { line: site.line - 1, character: site.column - 1 + site.length }
83
+ },
84
+ newText: fix.replacement_name
85
+ }
86
+ end
89
87
 
90
- actions << {
91
- title: "Rename '#{fix.original_name}' to '#{fix.replacement_name}'",
92
- kind: 'quickFix',
93
- diagnostics: [diag],
94
- edit: {
95
- changes: {
96
- uri => edits
88
+ actions << {
89
+ title: "Rename '#{fix.original_name}' to '#{fix.replacement_name}'",
90
+ kind: 'quickFix',
91
+ diagnostics: [diag],
92
+ edit: {
93
+ changes: {
94
+ uri => edits
95
+ }
97
96
  }
98
97
  }
99
- }
100
98
 
101
- when 'prefer-let', 'redundant-ignored-match-binding', 'prefer-let-else',
102
- 'prefer-var-else', 'redundant-bool-compare', 'redundant-else',
103
- 'redundant-return', 'unused-import', 'trailing-list-comma'
104
- fake_warning = FakeWarning.new(
105
- line: diag_line,
106
- column: diag_start_char + 1,
107
- length: (diag_end_char - diag_start_char).positive? ? diag_end_char - diag_start_char : nil,
108
- symbol_name: message[/'([^']+)'/, 1],
109
- )
110
- edits = Linter::FixEngine.edits_for_rule(code, lines, fake_warning)
111
- next if edits.empty?
112
-
113
- actions << {
114
- title: Linter.quick_fix_title(code),
115
- kind: 'quickFix',
116
- diagnostics: [diag],
117
- edit: { changes: { uri => Linter::FixEngine.edits_to_lsp_text_edits(edits, uri) } }
118
- }
99
+ when 'prefer-let', 'redundant-ignored-match-binding', 'prefer-let-else',
100
+ 'prefer-var-else', 'redundant-bool-compare', 'redundant-else',
101
+ 'redundant-return', 'unused-import', 'trailing-list-comma'
102
+ fake_warning = FakeWarning.new(
103
+ line: diag_line,
104
+ column: diag_start_char + 1,
105
+ length: (diag_end_char - diag_start_char).positive? ? diag_end_char - diag_start_char : nil,
106
+ symbol_name: message[/'([^']+)'/, 1],
107
+ )
108
+ edits = Linter::FixEngine.edits_for_rule(code, lines, fake_warning)
109
+ next if edits.empty?
119
110
 
120
- when 'line-too-long'
121
- fix = Formatter.build_long_line_wrap_fix(
122
- content,
123
- diag_line - 1,
124
- max_line_length: Formatter.resolve_max_line_length(uri),
125
- path: uri,
126
- )
127
- next unless fix
128
-
129
- actions << {
130
- title: Linter.quick_fix_title('line-too-long'),
131
- kind: 'quickFix',
132
- diagnostics: [diag],
133
- edit: {
134
- changes: {
135
- uri => [{
136
- range: {
137
- start: { line: fix[:start_line_idx], character: 0 },
138
- end: { line: fix[:end_line_idx] + 1, character: 0 }
139
- },
140
- newText: fix[:new_text]
141
- }]
142
- }
111
+ actions << {
112
+ title: Linter.quick_fix_title(code),
113
+ kind: 'quickFix',
114
+ diagnostics: [diag],
115
+ edit: { changes: { uri => Linter::FixEngine.edits_to_lsp_text_edits(edits, uri) } }
143
116
  }
144
- }
145
117
 
146
- when 'shadow'
147
- # Offer to rename to _ prefix; editor will invoke textDocument/rename.
148
- # This action just annotates — the actual rename is a client-side refactor.
149
- actions << {
150
- title: "Add '_' prefix to suppress shadow warning",
151
- kind: 'quickFix',
152
- diagnostics: [diag]
153
- }
118
+ when 'line-too-long'
119
+ fix = Formatter.build_long_line_wrap_fix(
120
+ content,
121
+ diag_line - 1,
122
+ max_line_length: Formatter.resolve_max_line_length(uri),
123
+ path: uri,
124
+ )
125
+ next unless fix
154
126
 
155
- when 'unused-param'
156
- next if source_line.empty?
157
-
158
- token = source_line[diag_start_char...diag_end_char].to_s
159
- token = token.strip
160
- next if token.empty?
161
- next if token.start_with?('_')
162
-
163
- replacement = "_#{token.gsub(/\A_+/, '')}"
164
- actions << {
165
- title: "Rename parameter '#{token}' to '#{replacement}'",
166
- kind: 'quickFix',
167
- diagnostics: [diag],
168
- edit: {
169
- changes: {
170
- uri => [{
171
- range: {
172
- start: { line: diag_line - 1, character: diag_start_char },
173
- end: { line: diag_line - 1, character: diag_end_char }
174
- },
175
- newText: replacement
176
- }]
127
+ actions << {
128
+ title: Linter.quick_fix_title('line-too-long'),
129
+ kind: 'quickFix',
130
+ diagnostics: [diag],
131
+ edit: {
132
+ changes: {
133
+ uri => [{
134
+ range: {
135
+ start: { line: fix[:start_line_idx], character: 0 },
136
+ end: { line: fix[:end_line_idx] + 1, character: 0 }
137
+ },
138
+ newText: fix[:new_text]
139
+ }]
140
+ }
177
141
  }
178
142
  }
179
- }
180
143
 
181
- when 'dead-assignment'
182
- next if source_line.empty?
183
-
184
- # Linter guarantees this write is overwritten before any read,
185
- # so dropping the statement is semantics-preserving.
186
- actions << {
187
- title: 'Remove dead assignment',
188
- kind: 'quickFix',
189
- diagnostics: [diag],
190
- edit: {
191
- changes: {
192
- uri => [{
193
- range: {
194
- start: { line: diag_line - 1, character: 0 },
195
- end: { line: diag_line, character: 0 }
196
- },
197
- newText: ''
198
- }]
199
- }
144
+ when 'shadow'
145
+ # Offer to rename to _ prefix; editor will invoke textDocument/rename.
146
+ # This action just annotates — the actual rename is a client-side refactor.
147
+ actions << {
148
+ title: "Add '_' prefix to suppress shadow warning",
149
+ kind: 'quickFix',
150
+ diagnostics: [diag]
200
151
  }
201
- }
202
152
 
203
- else
204
- next if source_line.empty?
153
+ when 'unused-param'
154
+ next if source_line.empty?
205
155
 
206
- # Wrap unsafe-required pointer casts into a local unsafe block.
207
- if message == 'pointer cast requires unsafe' || message == 'ref to pointer cast requires unsafe'
208
- next if source_line.match?(/\A\s*unsafe:\s*\z/)
156
+ token = source_line[diag_start_char...diag_end_char].to_s
157
+ token = token.strip
158
+ next if token.empty?
159
+ next if token.start_with?('_')
209
160
 
210
- indent = source_line[/\A\s*/] || ''
211
- body = source_line.sub(/\A\s*/, '').rstrip
212
- next if body.empty?
213
-
214
- wrapped = "#{indent}unsafe:\n#{indent} #{body}\n"
161
+ replacement = "_#{token.gsub(/\A_+/, '')}"
215
162
  actions << {
216
- title: 'Wrap statement in unsafe block',
163
+ title: "Rename parameter '#{token}' to '#{replacement}'",
217
164
  kind: 'quickFix',
218
165
  diagnostics: [diag],
219
166
  edit: {
220
167
  changes: {
221
168
  uri => [{
222
169
  range: {
223
- start: { line: diag_line - 1, character: 0 },
224
- end: { line: diag_line, character: 0 }
170
+ start: { line: diag_line - 1, character: diag_start_char },
171
+ end: { line: diag_line - 1, character: diag_end_char }
225
172
  },
226
- newText: wrapped
173
+ newText: replacement
227
174
  }]
228
175
  }
229
176
  }
230
177
  }
231
- end
232
-
233
- # "match on Foo is missing cases: A, B, C"
234
- if message =~ /\Amatch on (\S+) is missing cases: (.+)\z/
235
- _scrutinee_type = $1
236
- missing = $2.split(', ').map(&:strip).reject(&:empty?)
237
- next if missing.empty?
238
178
 
239
- indent = source_line[/\A\s*/] || ''
240
- arm_indent = indent + ' '
241
- body_indent = arm_indent + ' '
242
- new_arms = missing.map do |arm_name|
243
- "#{arm_indent}#{arm_name}:\n#{body_indent}return\n"
244
- end.join
245
-
246
- match_end_line = find_match_end_line(lines, diag_line - 1)
247
- insert_line = match_end_line || diag_line - 1
179
+ when 'dead-assignment'
180
+ next if source_line.empty?
248
181
 
182
+ # Linter guarantees this write is overwritten before any read,
183
+ # so dropping the statement is semantics-preserving.
249
184
  actions << {
250
- title: "Add missing match #{missing.length == 1 ? 'arm' : 'arms'}: #{missing.join(', ')}",
185
+ title: 'Remove dead assignment',
251
186
  kind: 'quickFix',
252
187
  diagnostics: [diag],
253
188
  edit: {
254
189
  changes: {
255
190
  uri => [{
256
191
  range: {
257
- start: { line: insert_line, character: 0 },
258
- end: { line: insert_line, character: 0 }
192
+ start: { line: diag_line - 1, character: 0 },
193
+ end: { line: diag_line, character: 0 }
259
194
  },
260
- newText: new_arms
195
+ newText: ''
261
196
  }]
262
197
  }
263
198
  }
264
199
  }
265
- end
266
200
 
267
- # "unknown type X" or "unknown name Y" with import suggestion
268
- if message =~ /\Aunknown (?:type|callable) (\S+)\z/
269
- _type_name = $1
270
- suggestion = diag.dig('data', 'suggestion')
271
- next unless suggestion.is_a?(String)
201
+ else
202
+ next if source_line.empty?
272
203
 
273
- if suggestion =~ /did you mean ['"]([^'"]+)['"]/
274
- full_path = $1
275
- parts = full_path.split('.')
276
- next unless parts.length >= 2
204
+ # Wrap unsafe-required pointer casts into a local unsafe block.
205
+ if message == 'pointer cast requires unsafe' || message == 'ref to pointer cast requires unsafe'
206
+ next if source_line.match?(/\A\s*unsafe:\s*\z/)
277
207
 
278
- import_module = parts[0..-2].join('.')
279
- _import_type = parts.last
208
+ indent = source_line[/\A\s*/] || ''
209
+ body = source_line.sub(/\A\s*/, '').rstrip
210
+ next if body.empty?
280
211
 
212
+ wrapped = "#{indent}unsafe:\n#{indent} #{body}\n"
281
213
  actions << {
282
- title: "Import #{full_path}",
214
+ title: 'Wrap statement in unsafe block',
283
215
  kind: 'quickFix',
284
216
  diagnostics: [diag],
285
217
  edit: {
286
218
  changes: {
287
219
  uri => [{
288
220
  range: {
289
- start: { line: 0, character: 0 },
290
- end: { line: 0, character: 0 }
221
+ start: { line: diag_line - 1, character: 0 },
222
+ end: { line: diag_line, character: 0 }
291
223
  },
292
- newText: "import #{import_module}\n"
224
+ newText: wrapped
293
225
  }]
294
226
  }
295
227
  }
296
228
  }
297
229
  end
230
+
231
+ # "match on Foo is missing cases: A, B, C"
232
+ if message =~ /\Amatch on (\S+) is missing cases: (.+)\z/
233
+ _scrutinee_type = $1
234
+ missing = $2.split(', ').map(&:strip).reject(&:empty?)
235
+ next if missing.empty?
236
+
237
+ indent = source_line[/\A\s*/] || ''
238
+ arm_indent = indent + ' '
239
+ body_indent = arm_indent + ' '
240
+ new_arms = missing.map do |arm_name|
241
+ "#{arm_indent}#{arm_name}:\n#{body_indent}return\n"
242
+ end.join
243
+
244
+ match_end_line = find_match_end_line(lines, diag_line - 1)
245
+ insert_line = match_end_line || diag_line - 1
246
+
247
+ actions << {
248
+ title: "Add missing match #{missing.length == 1 ? 'arm' : 'arms'}: #{missing.join(', ')}",
249
+ kind: 'quickFix',
250
+ diagnostics: [diag],
251
+ edit: {
252
+ changes: {
253
+ uri => [{
254
+ range: {
255
+ start: { line: insert_line, character: 0 },
256
+ end: { line: insert_line, character: 0 }
257
+ },
258
+ newText: new_arms
259
+ }]
260
+ }
261
+ }
262
+ }
263
+ end
264
+
265
+ # "unknown type X" or "unknown name Y" with import suggestion
266
+ if message =~ /\Aunknown (?:type|callable) (\S+)\z/
267
+ _type_name = $1
268
+ suggestion = diag.dig('data', 'suggestion')
269
+ next unless suggestion.is_a?(String)
270
+
271
+ if suggestion =~ /did you mean ['"]([^'"]+)['"]/
272
+ full_path = $1
273
+ parts = full_path.split('.')
274
+ next unless parts.length >= 2
275
+
276
+ import_module = parts[0..-2].join('.')
277
+ _import_type = parts.last
278
+
279
+ actions << {
280
+ title: "Import #{full_path}",
281
+ kind: 'quickFix',
282
+ diagnostics: [diag],
283
+ edit: {
284
+ changes: {
285
+ uri => [{
286
+ range: {
287
+ start: { line: 0, character: 0 },
288
+ end: { line: 0, character: 0 }
289
+ },
290
+ newText: "import #{import_module}\n"
291
+ }]
292
+ }
293
+ }
294
+ }
295
+ end
296
+ end
298
297
  end
299
298
  end
300
- end
301
- end # want_quickfix
302
- quickfix_ms = elapsed_ms(quickfix_start)
303
-
304
- # ── source.fixAll: apply all lint auto-fixes at once ───────────────
305
- # Skip for files outside the workspace root (library files) and very
306
- # large files to keep codeAction latency bounded.
307
- fixall_ms = 0.0
308
- fixall_skipped_reason = want_fixall ? skip_expensive_work_reason(uri, content) : 'not-requested'
309
- unless fixall_skipped_reason
310
- fixall_start = monotonic_time
311
- begin
312
- content_hash = content.hash
313
- cached_fixall = @fixall_cache[uri]
314
- if cached_fixall &&
315
- cached_fixall[:content_hash] == content_hash
316
- fixed = cached_fixall[:fixed]
317
- else
318
- fixed = begin
319
- Linter.fix_source(content, path: uri)
320
- rescue StandardError
321
- content
299
+ end # want_quickfix
300
+ quickfix_ms = elapsed_ms(quickfix_start)
301
+
302
+ # ── source.fixAll: apply all lint auto-fixes at once ───────────────
303
+ # Skip for files outside the workspace root (library files) and very
304
+ # large files to keep codeAction latency bounded.
305
+ fixall_ms = 0.0
306
+ fixall_skipped_reason = want_fixall ? skip_expensive_work_reason(uri, content) : 'not-requested'
307
+ unless fixall_skipped_reason
308
+ fixall_start = monotonic_time
309
+ begin
310
+ content_hash = content.hash
311
+ cached_fixall = @fixall_cache[uri]
312
+ if cached_fixall &&
313
+ cached_fixall[:content_hash] == content_hash
314
+ fixed = cached_fixall[:fixed]
315
+ else
316
+ fixed = begin
317
+ Linter.fix_source(content, path: uri)
318
+ rescue StandardError
319
+ content
320
+ end
321
+ @fixall_cache[uri] = {
322
+ content_hash: content_hash,
323
+ fixed: fixed,
324
+ }
322
325
  end
323
- @fixall_cache[uri] = {
324
- content_hash: content_hash,
325
- fixed: fixed,
326
- }
327
- end
328
- if fixed != content
329
- line_count = content.count("\n")
330
- actions << {
331
- title: Linter::FIX_ALL_TITLE,
332
- kind: 'source.fixAll',
333
- edit: {
334
- changes: {
335
- uri => [{
336
- range: {
337
- start: { line: 0, character: 0 },
338
- end: { line: line_count + 1, character: 0 }
339
- },
340
- newText: fixed
341
- }]
326
+ if fixed != content
327
+ line_count = content.count("\n")
328
+ actions << {
329
+ title: Linter::FIX_ALL_TITLE,
330
+ kind: 'source.fixAll',
331
+ edit: {
332
+ changes: {
333
+ uri => [{
334
+ range: {
335
+ start: { line: 0, character: 0 },
336
+ end: { line: line_count + 1, character: 0 }
337
+ },
338
+ newText: fixed
339
+ }]
340
+ }
342
341
  }
343
342
  }
344
- }
343
+ end
344
+ rescue StandardError => e
345
+ warn "Error building source.fixAll action: #{e.message}"
345
346
  end
346
- rescue StandardError => e
347
- warn "Error building source.fixAll action: #{e.message}"
347
+ fixall_ms = elapsed_ms(fixall_start)
348
348
  end
349
- fixall_ms = elapsed_ms(fixall_start)
349
+
350
+ elapsed = elapsed_ms(total_start)
351
+ short_uri = shorten_uri(uri) || uri
352
+ fixall_detail = fixall_skipped_reason ? "skipped(#{fixall_skipped_reason})" : "generated(ms=#{fixall_ms})"
353
+ log_perf_breakdown('textDocument/codeAction', elapsed,
354
+ "uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} diagnostics=#{requested_diagnostics.length} actions=#{actions.length} fixAll=#{fixall_detail} stages_ms=quickfix:#{quickfix_ms},fixAll:#{fixall_ms}")
355
+
356
+ actions
357
+ rescue StandardError => e
358
+ warn "Error in codeAction handler: #{e.message}"
359
+ []
350
360
  end
351
361
 
352
- elapsed = elapsed_ms(total_start)
353
- short_uri = shorten_uri(uri) || uri
354
- fixall_detail = fixall_skipped_reason ? "skipped(#{fixall_skipped_reason})" : "generated(ms=#{fixall_ms})"
355
- log_perf_breakdown('textDocument/codeAction', elapsed,
356
- "uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} diagnostics=#{requested_diagnostics.length} actions=#{actions.length} fixAll=#{fixall_detail} stages_ms=quickfix:#{quickfix_ms},fixAll:#{fixall_ms}")
362
+ def handle_document_diagnostic(params)
363
+ uri = params.dig('textDocument', 'uri')
364
+ return { kind: 'full', items: [] } unless uri
357
365
 
358
- actions
359
- rescue StandardError => e
360
- warn "Error in codeAction handler: #{e.message}"
361
- []
362
- end
366
+ content = @workspace.get_content(uri)
367
+ result = @workspace.collect_diagnostics(uri)
368
+ diagnostics = result
369
+ fingerprint = diagnostics_fingerprint(content, diagnostics)
370
+ previous_result_id = params['previousResultId']
371
+ cached = @diagnostic_report_cache[uri]
372
+
373
+ if cached && cached[:result_id] == previous_result_id && cached[:fingerprint] == fingerprint
374
+ return {
375
+ kind: 'unchanged',
376
+ resultId: cached[:result_id]
377
+ }
378
+ end
363
379
 
364
- def handle_document_diagnostic(params)
365
- uri = params.dig('textDocument', 'uri')
366
- return { kind: 'full', items: [] } unless uri
367
-
368
- content = @workspace.get_content(uri)
369
- result = @workspace.collect_diagnostics(uri)
370
- diagnostics = result
371
- fingerprint = diagnostics_fingerprint(content, diagnostics)
372
- previous_result_id = params['previousResultId']
373
- cached = @diagnostic_report_cache[uri]
374
-
375
- if cached && cached[:result_id] == previous_result_id && cached[:fingerprint] == fingerprint
376
- return {
377
- kind: 'unchanged',
378
- resultId: cached[:result_id]
380
+ result_id = next_diagnostic_result_id(uri, fingerprint)
381
+ @diagnostic_report_cache[uri] = {
382
+ result_id: result_id,
383
+ fingerprint: fingerprint
379
384
  }
380
- end
381
385
 
382
- result_id = next_diagnostic_result_id(uri, fingerprint)
383
- @diagnostic_report_cache[uri] = {
384
- result_id: result_id,
385
- fingerprint: fingerprint
386
- }
387
-
388
- cross = @workspace.cross_file_diagnostics
389
- if cross&.any?
390
- cross.each do |target_uri, items|
391
- @protocol.write_notification('textDocument/publishDiagnostics', {
392
- uri: target_uri,
393
- diagnostics: items
394
- })
386
+ cross = @workspace.cross_file_diagnostics
387
+ if cross&.any?
388
+ cross.each do |target_uri, items|
389
+ @protocol.write_notification('textDocument/publishDiagnostics', {
390
+ uri: target_uri,
391
+ diagnostics: items
392
+ })
393
+ end
395
394
  end
396
- end
397
395
 
398
- {
399
- kind: 'full',
400
- resultId: result_id,
401
- items: diagnostics
402
- }
403
- rescue StandardError => e
404
- warn "Error in documentDiagnostic handler: #{e.message}"
405
- { kind: 'full', items: [] }
406
- end
407
-
408
- def handle_workspace_diagnostic(params)
409
- previous_ids = params['previousResultIds'] || []
410
- prev_map = previous_ids.each_with_object({}) do |entry, h|
411
- h[entry['uri']] = entry['value'] if entry.is_a?(Hash) && entry['uri']
396
+ {
397
+ kind: 'full',
398
+ resultId: result_id,
399
+ items: diagnostics
400
+ }
401
+ rescue StandardError => e
402
+ warn "Error in documentDiagnostic handler: #{e.message}"
403
+ { kind: 'full', items: [] }
412
404
  end
413
405
 
414
- all_uris = @workspace.open_document_uris
415
- items = all_uris.filter_map do |uri|
416
- content = @workspace.get_content(uri)
417
- next if content.empty?
406
+ def handle_workspace_diagnostic(params)
407
+ previous_ids = params['previousResultIds'] || []
408
+ prev_map = previous_ids.each_with_object({}) do |entry, h|
409
+ h[entry['uri']] = entry['value'] if entry.is_a?(Hash) && entry['uri']
410
+ end
418
411
 
419
- diagnostics = @workspace.collect_diagnostics(uri)
420
- fingerprint = diagnostics_fingerprint(content, diagnostics)
421
- result_id = next_diagnostic_result_id(uri, fingerprint)
412
+ all_uris = @workspace.open_document_uris
413
+ items = all_uris.filter_map do |uri|
414
+ content = @workspace.get_content(uri)
415
+ next if content.empty?
422
416
 
423
- cached = @workspace_diagnostic_cache[uri]
424
- if cached && cached[:result_id] == prev_map[uri] && cached[:fingerprint] == fingerprint
425
- { uri: uri, kind: 'unchanged', resultId: result_id, items: [] }
426
- else
427
- @workspace_diagnostic_cache[uri] = { result_id: result_id, fingerprint: fingerprint }
428
- { uri: uri, kind: 'full', resultId: result_id, items: diagnostics }
417
+ diagnostics = @workspace.collect_diagnostics(uri)
418
+ fingerprint = diagnostics_fingerprint(content, diagnostics)
419
+ result_id = next_diagnostic_result_id(uri, fingerprint)
420
+
421
+ cached = @workspace_diagnostic_cache[uri]
422
+ if cached && cached[:result_id] == prev_map[uri] && cached[:fingerprint] == fingerprint
423
+ { uri: uri, kind: 'unchanged', resultId: result_id, items: [] }
424
+ else
425
+ @workspace_diagnostic_cache[uri] = { result_id: result_id, fingerprint: fingerprint }
426
+ { uri: uri, kind: 'full', resultId: result_id, items: diagnostics }
427
+ end
429
428
  end
430
- end
431
429
 
432
- { items: items }
433
- rescue StandardError => e
434
- warn "Error in workspace/diagnostic handler: #{e.message}"
435
- { items: [] }
436
- end
430
+ { items: items }
431
+ rescue StandardError => e
432
+ warn "Error in workspace/diagnostic handler: #{e.message}"
433
+ { items: [] }
434
+ end
437
435
 
438
- def refresh_workspace_diagnostics
439
- @protocol.write_notification('workspace/diagnostic/refresh', nil)
440
- end
436
+ def refresh_workspace_diagnostics
437
+ @protocol.write_notification('workspace/diagnostic/refresh', nil)
438
+ end
441
439
 
442
- def find_match_end_line(lines, match_start_idx)
443
- return nil if match_start_idx >= lines.length
440
+ def find_match_end_line(lines, match_start_idx)
441
+ return nil if match_start_idx >= lines.length
444
442
 
445
- match_indent = lines[match_start_idx][/\A\s*/].length
446
- (match_start_idx + 1...lines.length).each do |i|
447
- line = lines[i]
448
- next if line.strip.empty?
449
- line_indent = line[/\A\s*/].length
450
- return i if line_indent <= match_indent
443
+ match_indent = lines[match_start_idx][/\A\s*/].length
444
+ (match_start_idx + 1...lines.length).each do |i|
445
+ line = lines[i]
446
+ next if line.strip.empty?
447
+ line_indent = line[/\A\s*/].length
448
+ return i if line_indent <= match_indent
449
+ end
450
+ nil
451
451
  end
452
- nil
453
- end
454
452
  end
455
453
  end
456
454
  end