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,593 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Linter
5
+ module LinterRules
6
+ private
7
+
8
+ # ── missing-return ───────────────────────────────────────────────────
9
+
10
+ def check_missing_return(function)
11
+ return unless function.return_type # implicit void — no check
12
+ return if void_return_type?(function.return_type)
13
+ return if always_returns?(function.body)
14
+
15
+ @warnings << Warning.new(
16
+ path: @path,
17
+ line: function.line,
18
+ column: function.respond_to?(:column) ? function.column : nil,
19
+ length: function.name.length,
20
+ code: "missing-return",
21
+ message: "function '#{function.name}' does not always return a value",
22
+ severity: :error,
23
+ symbol_name: function.name
24
+ )
25
+ end
26
+
27
+ def emit_redundant_return_warnings(function)
28
+ return unless function.return_type
29
+ return unless void_return_type?(function.return_type)
30
+
31
+ final_statement = function.body&.last
32
+ return unless final_statement.is_a?(AST::ReturnStmt)
33
+ return unless final_statement.value.nil?
34
+
35
+ @warnings << Warning.new(
36
+ path: @path,
37
+ line: final_statement.line,
38
+ column: final_statement.column,
39
+ length: final_statement.length || "return".length,
40
+ code: "redundant-return",
41
+ message: "final bare return in void function is redundant",
42
+ severity: :hint,
43
+ )
44
+ end
45
+
46
+ def void_return_type?(return_type)
47
+ case return_type
48
+ when AST::TypeRef
49
+ name = return_type.name
50
+ name.is_a?(AST::QualifiedName) && name.parts == ["void"]
51
+ when Types::Primitive
52
+ return_type.name == "void"
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ # Returns true if every execution path through `stmts` ends with a
59
+ # guaranteed return. Conservative: only IfStmt with else + MatchStmt
60
+ # with arms are considered exhaustive.
61
+ def always_returns?(stmts)
62
+ stmts.any? do |stmt|
63
+ case stmt
64
+ when AST::ReturnStmt
65
+ true
66
+ when AST::ExpressionStmt
67
+ terminating_expression?(stmt.expression)
68
+ when AST::IfStmt
69
+ # Only exhaustive if there is an else branch AND every branch returns
70
+ stmt.else_body && !stmt.else_body.empty? &&
71
+ stmt.branches.all? { |b| always_returns?(b.body) } &&
72
+ always_returns?(stmt.else_body)
73
+ when AST::WhileStmt
74
+ infinite_while_without_break?(stmt)
75
+ when AST::MatchStmt
76
+ stmt.arms.any? && stmt.arms.all? { |arm| always_returns?(arm.body) }
77
+ when AST::WhenStmt
78
+ all_branches = stmt.branches.map(&:body)
79
+ all_branches << stmt.else_body if stmt.else_body && !stmt.else_body.empty?
80
+ all_branches.any? && all_branches.all? { |b| always_returns?(b) }
81
+ when AST::StaticAssert
82
+ stmt.condition.is_a?(AST::BooleanLiteral) && stmt.condition.value == false
83
+ when AST::UnsafeStmt
84
+ always_returns?(stmt.body)
85
+ else
86
+ false
87
+ end
88
+ end
89
+ end
90
+
91
+ def infinite_while_without_break?(stmt)
92
+ stmt.condition.is_a?(AST::BooleanLiteral) &&
93
+ stmt.condition.value == true &&
94
+ !loop_body_can_break?(stmt.body)
95
+ end
96
+
97
+ def loop_body_can_break?(body)
98
+ return false if body.nil? || body.empty?
99
+
100
+ graph = ControlFlow::Builder.new.build_loop_body(body)
101
+ reachability = ControlFlow::Reachability.solve(graph)
102
+ graph.each_node.any? do |node|
103
+ node.kind == :break_exit && reachability.reachable_ids.include?(node.id)
104
+ end
105
+ end
106
+
107
+ def terminating_expression?(expression)
108
+ case expression
109
+ when AST::Call
110
+ terminating_callee?(expression.callee) || static_assert_false?(expression)
111
+ when AST::Specialization
112
+ terminating_callee?(expression.callee) || static_assert_false?(expression)
113
+ else
114
+ false
115
+ end
116
+ end
117
+
118
+ def static_assert_false?(expression)
119
+ return false unless expression.callee.is_a?(AST::Identifier)
120
+ return false unless expression.callee.name == "static_assert"
121
+
122
+ first_arg = expression.arguments.first
123
+ first_arg.is_a?(AST::BooleanLiteral) && first_arg.value == false
124
+ end
125
+
126
+ def terminating_callee?(callee)
127
+ case callee
128
+ when AST::Identifier
129
+ callee.name == "fatal"
130
+ when AST::Specialization
131
+ terminating_callee?(callee.callee)
132
+ else
133
+ false
134
+ end
135
+ end
136
+ # ── redundant-else ───────────────────────────────────────────────────
137
+ # Fire when every explicit if/else if branch always returns, making the else
138
+ # block an unnecessary level of indentation.
139
+ def check_redundant_else(stmt)
140
+ return unless stmt.else_body && !stmt.else_body.empty?
141
+ return unless stmt.branches.all? { |b| always_returns?(b.body) }
142
+
143
+ # Use the line of the first else-body statement as the diagnostic anchor.
144
+ else_line = stmt.else_body.first.respond_to?(:line) ? stmt.else_body.first.line : stmt.line
145
+ @warnings << Warning.new(
146
+ path: @path,
147
+ line: stmt.else_line || else_line,
148
+ column: stmt.else_column,
149
+ length: 4,
150
+ code: "redundant-else",
151
+ message: "else block is redundant because all preceding branches return"
152
+ )
153
+ end
154
+ # ── useless-expression ───────────────────────────────────────────────────
155
+
156
+ PURE_EXPRESSION_TYPES = [
157
+ AST::IntegerLiteral, AST::FloatLiteral, AST::StringLiteral, AST::FormatString,
158
+ AST::BooleanLiteral, AST::NullLiteral,
159
+ AST::BinaryOp, AST::UnaryOp,
160
+ AST::Identifier, AST::UnsafeExpr,
161
+ ].freeze
162
+
163
+ SIDE_EFFECTING_EXPRESSION_TYPES = [
164
+ AST::Call, AST::AwaitExpr,
165
+ ].freeze
166
+
167
+ def check_useless_expression(stmt)
168
+ expr = stmt.expression
169
+ return unless PURE_EXPRESSION_TYPES.any? { |t| expr.is_a?(t) }
170
+ return if expr.is_a?(AST::UnaryOp) && expr.operator == "?"
171
+ return if contains_side_effecting_expression?(expr)
172
+
173
+ line = expression_line(expr) || (stmt.respond_to?(:line) ? stmt.line : nil)
174
+ column = expression_column(expr)
175
+ length = expression_length(expr)
176
+ if line && (!column || !length || !expr.respond_to?(:column) || expr.is_a?(AST::UnsafeExpr))
177
+ fallback_span = source_statement_span(line)
178
+ column ||= fallback_span&.first
179
+ length = fallback_span&.last if !length || !expr.respond_to?(:column) || expr.is_a?(AST::UnsafeExpr)
180
+ end
181
+
182
+ @warnings << Warning.new(
183
+ path: @path,
184
+ line:,
185
+ column:,
186
+ length:,
187
+ code: "useless-expression",
188
+ message: "expression result is unused and has no side effects",
189
+ severity: :warning
190
+ )
191
+ end
192
+
193
+ def contains_side_effecting_expression?(expr)
194
+ return true if SIDE_EFFECTING_EXPRESSION_TYPES.any? { |t| expr.is_a?(t) }
195
+ return true if expr.is_a?(AST::UnaryOp) && expr.operator == "?"
196
+
197
+ case expr
198
+ when AST::BinaryOp
199
+ contains_side_effecting_expression?(expr.left) || contains_side_effecting_expression?(expr.right)
200
+ when AST::UnsafeExpr
201
+ contains_side_effecting_expression?(expr.expression)
202
+ when AST::FormatString
203
+ expr.parts.any? { |part| part.is_a?(AST::FormatExprPart) && contains_side_effecting_expression?(part.expression) }
204
+ else
205
+ false
206
+ end
207
+ end
208
+ # ── self-assignment ────────────────────────────────────────────────────
209
+
210
+ def check_self_assignment(stmt)
211
+ return unless stmt.operator == "="
212
+ return unless stmt.target.is_a?(AST::Identifier) && stmt.value.is_a?(AST::Identifier)
213
+ return unless stmt.target.name == stmt.value.name
214
+
215
+ @warnings << Warning.new(
216
+ path: @path,
217
+ line: expression_line(stmt.target) || stmt.line,
218
+ column: expression_column(stmt.target),
219
+ length: expression_length(stmt.target),
220
+ code: "self-assignment",
221
+ message: "'#{stmt.target.name}' is assigned to itself",
222
+ severity: :warning,
223
+ symbol_name: stmt.target.name
224
+ )
225
+ end
226
+
227
+ # ── self-comparison ────────────────────────────────────────────────────
228
+
229
+ def check_self_comparison(expr)
230
+ return unless %w[== !=].include?(expr.operator)
231
+ return unless expr.left.is_a?(AST::Identifier) && expr.right.is_a?(AST::Identifier)
232
+ return unless expr.left.name == expr.right.name
233
+
234
+ line = expression_line(expr) || expr.left.line
235
+ always = expr.operator == "==" ? "always true" : "always false"
236
+ @warnings << Warning.new(
237
+ path: @path,
238
+ line:,
239
+ column: expression_column(expr),
240
+ length: expression_length(expr),
241
+ code: "self-comparison",
242
+ message: "'#{expr.left.name}' is compared to itself — #{always}",
243
+ severity: :warning,
244
+ symbol_name: expr.left.name
245
+ )
246
+ end
247
+
248
+ def check_redundant_bool_compare(expr)
249
+ return unless %w[== !=].include?(expr.operator)
250
+
251
+ left_bool = expr.left.is_a?(AST::BooleanLiteral)
252
+ right_bool = expr.right.is_a?(AST::BooleanLiteral)
253
+ return unless left_bool ^ right_bool
254
+
255
+ literal = left_bool ? expr.left : expr.right
256
+ compared = left_bool ? expr.right : expr.left
257
+ return if compared.is_a?(AST::BooleanLiteral)
258
+
259
+ suggestion = if expr.operator == "=="
260
+ literal.value ? "use the expression directly" : "invert the expression with 'not'"
261
+ else
262
+ literal.value ? "invert the expression with 'not'" : "use the expression directly"
263
+ end
264
+
265
+ @warnings << Warning.new(
266
+ path: @path,
267
+ line: expression_line(expr),
268
+ column: expression_column(expr),
269
+ length: redundant_bool_compare_length(expr),
270
+ code: "redundant-bool-compare",
271
+ message: "boolean comparison against literal is redundant; #{suggestion}",
272
+ severity: :hint,
273
+ )
274
+ end
275
+
276
+ def redundant_bool_compare_length(expr)
277
+ length = expression_length(expr)
278
+ return length if length
279
+
280
+ line = expression_line(expr)
281
+ column = expression_column(expr)
282
+ return nil unless line && column
283
+
284
+ text = source_code_line(line)
285
+ return nil unless text
286
+
287
+ snippet = text[(column - 1)..]
288
+ return nil unless snippet
289
+
290
+ right_literal = snippet.match(/\A\s*(.+?)\s*(==|!=)\s*(true|false)\b/)
291
+ return right_literal[0].rstrip.length if right_literal
292
+
293
+ left_literal = snippet.match(/\A\s*(true|false)\s*(==|!=)\s*(.+?)(?=\s*(?::|\)|,|and\b|or\b|$))/)
294
+ return left_literal[0].rstrip.length if left_literal
295
+
296
+ nil
297
+ end
298
+
299
+ def check_duplicate_if_conditions(statement)
300
+ return unless statement.is_a?(AST::IfStmt)
301
+
302
+ seen_signatures = {}
303
+ statement.branches.each do |branch|
304
+ signature = expression_signature(branch.condition)
305
+ next unless signature
306
+
307
+ existing = seen_signatures[signature]
308
+ if existing
309
+ @warnings << Warning.new(
310
+ path: @path,
311
+ line: expression_line(branch.condition) || branch.line,
312
+ column: expression_column(branch.condition),
313
+ length: expression_length(branch.condition),
314
+ code: "duplicate-if-condition",
315
+ message: "duplicate condition matches an earlier if/else-if branch and is unreachable",
316
+ severity: :warning,
317
+ )
318
+ next
319
+ end
320
+
321
+ seen_signatures[signature] = branch
322
+ end
323
+ end
324
+
325
+ def expression_signature(expression)
326
+ case expression
327
+ when AST::Identifier
328
+ "id:#{expression.name}"
329
+ when AST::BooleanLiteral
330
+ "bool:#{expression.value}"
331
+ when AST::IntegerLiteral, AST::FloatLiteral, AST::StringLiteral
332
+ "lit:#{expression.lexeme}"
333
+ when AST::NullLiteral
334
+ "null"
335
+ when AST::MemberAccess
336
+ receiver = expression_signature(expression.receiver)
337
+ receiver ? "member:(#{receiver}).#{expression.member}" : nil
338
+ when AST::UnaryOp
339
+ operand = expression_signature(expression.operand)
340
+ operand ? "unary:#{expression.operator}(#{operand})" : nil
341
+ when AST::BinaryOp
342
+ left = expression_signature(expression.left)
343
+ right = expression_signature(expression.right)
344
+ return nil unless left && right
345
+
346
+ "binary:(#{left})#{expression.operator}(#{right})"
347
+ else
348
+ nil
349
+ end
350
+ end
351
+
352
+ def check_noop_compound_assignment(statement)
353
+ return unless statement.is_a?(AST::Assignment)
354
+ return unless %w[+= -= *= /= |= ^= <<= >>=].include?(statement.operator)
355
+
356
+ identity = noop_compound_identity_value(statement.operator, statement.value)
357
+ return unless identity
358
+
359
+ @warnings << Warning.new(
360
+ path: @path,
361
+ line: statement.line,
362
+ column: expression_column(statement.target),
363
+ length: statement.target.respond_to?(:name) ? statement.target.name.length : expression_length(statement.target),
364
+ code: "noop-compound-assignment",
365
+ message: "compound assignment with identity value has no effect",
366
+ severity: :hint,
367
+ )
368
+ end
369
+
370
+ def noop_compound_identity_value(operator, value)
371
+ case operator
372
+ when "+=", "-=", "|=", "^=", "<<=", ">>="
373
+ integer_literal_zero?(value)
374
+ when "*=", "/="
375
+ numeric_literal_one?(value)
376
+ else
377
+ false
378
+ end
379
+ end
380
+
381
+ def integer_literal_zero?(value)
382
+ value.is_a?(AST::IntegerLiteral) && value.lexeme.gsub("_", "") == "0"
383
+ end
384
+
385
+ def numeric_literal_one?(value)
386
+ return true if value.is_a?(AST::IntegerLiteral) && value.lexeme.gsub("_", "") == "1"
387
+ return true if value.is_a?(AST::FloatLiteral) && %w[1.0 1.].include?(value.lexeme.gsub("_", ""))
388
+
389
+ false
390
+ end
391
+ # ── prefer-let-else / prefer-var-else ──────────────────────────────
392
+
393
+ def emit_prefer_let_else_warnings(stmts)
394
+ emit_prefer_else_warnings(stmts, expected_kind: :let, code: "prefer-let-else")
395
+ end
396
+
397
+ def emit_prefer_var_else_warnings(stmts)
398
+ emit_prefer_else_warnings(stmts, expected_kind: :var, code: "prefer-var-else")
399
+ end
400
+
401
+ def emit_prefer_else_warnings(stmts, expected_kind:, code:)
402
+ return if stmts.nil? || stmts.empty?
403
+
404
+ walk_statement_lists(stmts) do |statement_list|
405
+ statement_list.each_with_index do |_statement, index|
406
+ candidate = prefer_else_candidate(statement_list, index, expected_kind:)
407
+ next unless candidate
408
+
409
+ branch = candidate[:branch]
410
+ line, column, length = condition_span(branch.condition, line: candidate[:if_stmt].line, keyword_pattern: "if")
411
+
412
+ @warnings << Warning.new(
413
+ path: @path,
414
+ line:,
415
+ column:,
416
+ length:,
417
+ code:,
418
+ message: "nullable guard for '#{candidate[:declaration].name}' can use #{expected_kind} ... else",
419
+ severity: :hint,
420
+ symbol_name: candidate[:declaration].name
421
+ )
422
+ end
423
+ end
424
+ end
425
+
426
+ def prefer_else_candidate(stmts, index, expected_kind:)
427
+ declaration = stmts[index]
428
+ if_stmt = stmts[index + 1]
429
+ return unless declaration.is_a?(AST::LocalDecl)
430
+ return unless declaration.kind == expected_kind
431
+ return if declaration.type || declaration.else_binding || declaration.else_body || declaration.recovered_else
432
+ return unless declaration.value && declaration.name
433
+ return if ignored_binding_name?(declaration.name)
434
+ return unless if_stmt.is_a?(AST::IfStmt)
435
+ return unless if_stmt.else_body.nil? && if_stmt.branches.length == 1
436
+
437
+ branch = if_stmt.branches.first
438
+ identifier = null_equality_identifier(branch.condition)
439
+ return unless identifier&.name == declaration.name
440
+ return unless nullable_binding_declaration?(declaration)
441
+ return if expected_kind == :var && !prefer_var_else_binding_mutated?(declaration)
442
+ return unless ControlFlow::Termination.block_always_terminates?(branch.body, ignore_name: method(:ignored_binding_name?), binding_resolution: cfg_binding_resolution)
443
+
444
+ { declaration:, if_stmt:, branch: }
445
+ end
446
+
447
+ def prefer_var_else_binding_mutated?(declaration)
448
+ binding = @scopes.last[declaration.name]
449
+ binding&.mutated == true
450
+ end
451
+
452
+ def null_equality_identifier(cond)
453
+ return nil unless cond.is_a?(AST::BinaryOp) && cond.operator == "=="
454
+
455
+ if cond.left.is_a?(AST::Identifier) && cond.right.is_a?(AST::NullLiteral)
456
+ cond.left
457
+ elsif cond.left.is_a?(AST::NullLiteral) && cond.right.is_a?(AST::Identifier)
458
+ cond.right
459
+ end
460
+ end
461
+
462
+ def nullable_binding_declaration?(statement)
463
+ binding_type = binding_type_for_declaration(statement)
464
+ return binding_type.is_a?(Types::Nullable) if binding_type
465
+
466
+ nullable_initializer_without_binding_type?(statement.value)
467
+ end
468
+
469
+ def nullable_initializer_without_binding_type?(expression)
470
+ case expression
471
+ when AST::Identifier, AST::MemberAccess, AST::IndexAccess, AST::Call, AST::IfExpr, AST::MatchExpr
472
+ true
473
+ when AST::AwaitExpr, AST::UnsafeExpr
474
+ nullable_initializer_without_binding_type?(expression.expression)
475
+ else
476
+ false
477
+ end
478
+ end
479
+
480
+ def binding_type_for_declaration(statement)
481
+ binding_resolution = @sema_facts&.binding_resolution
482
+ return nil unless binding_resolution&.binding_types
483
+
484
+ binding_id = binding_resolution.declaration_binding_ids[statement.object_id]
485
+ return nil unless binding_id
486
+
487
+ binding_resolution.binding_types[binding_id]
488
+ end
489
+ def pointer_like_cast_expression(expression)
490
+ return unless expression.is_a?(AST::PrefixCast)
491
+
492
+ target_type = expression.target_type
493
+ return unless target_type.is_a?(AST::TypeRef)
494
+ return unless pointer_like_type_ref?(target_type)
495
+
496
+ { target_type:, source: expression.expression }
497
+ end
498
+
499
+ def pointer_like_type_ref?(type_ref)
500
+ return false if type_ref.nullable
501
+
502
+ %w[ptr const_ptr ref].include?(type_ref.name.to_s)
503
+ end
504
+ # ── directional-ffi-arg ──────────────────────────────────────────────
505
+
506
+ def check_directional_ffi_call(expression)
507
+ call = resolve_directional_ffi_call(expression.callee)
508
+ return unless call
509
+
510
+ call[:params].zip(expression.arguments).each do |parameter, argument|
511
+ next unless parameter && argument
512
+
513
+ passing_mode = parameter_passing_mode(parameter)
514
+ next unless %i[in out inout].include?(passing_mode)
515
+ next unless legacy_directional_argument_expression?(argument.value)
516
+
517
+ @warnings << Warning.new(
518
+ path: @path,
519
+ line: expression_line(argument.value),
520
+ column: expression_column(argument.value),
521
+ length: expression_length(argument.value),
522
+ code: "directional-ffi-arg",
523
+ message: "pass the lvalue directly to '#{call[:name]}'; parameter '#{parameter_name(parameter)}' already declares #{passing_mode} passing",
524
+ severity: :hint,
525
+ symbol_name: parameter_name(parameter)
526
+ )
527
+ end
528
+ end
529
+
530
+ def resolve_directional_ffi_call(callee)
531
+ case callee
532
+ when AST::Specialization
533
+ resolve_directional_ffi_call(callee.callee)
534
+ when AST::Identifier
535
+ if @sema_facts && (binding = @sema_facts.functions[callee.name]) && directional_ffi_binding?(binding)
536
+ return { name: binding.name, params: binding.type.params }
537
+ end
538
+
539
+ if (declaration = @declared_directional_functions[callee.name])
540
+ return { name: declaration.name, params: declaration.params }
541
+ end
542
+
543
+ nil
544
+ when AST::MemberAccess
545
+ return nil unless callee.receiver.is_a?(AST::Identifier)
546
+ return nil unless @sema_facts
547
+
548
+ imported_module = @sema_facts.imports[callee.receiver.name]
549
+ return nil unless imported_module
550
+
551
+ binding = imported_module.functions[callee.member]
552
+ return nil unless directional_ffi_binding?(binding)
553
+
554
+ { name: binding.name, params: binding.type.params }
555
+ else
556
+ nil
557
+ end
558
+ end
559
+
560
+ def directional_ffi_binding?(binding)
561
+ return false unless binding
562
+ return false unless binding.respond_to?(:ast) && (binding.ast.is_a?(AST::ExternFunctionDecl) || binding.ast.is_a?(AST::ForeignFunctionDecl))
563
+
564
+ binding.type.params.any? { |parameter| %i[in out inout].include?(parameter.passing_mode) }
565
+ end
566
+
567
+ def parameter_passing_mode(parameter)
568
+ parameter.respond_to?(:passing_mode) ? parameter.passing_mode : parameter.mode
569
+ end
570
+
571
+ def parameter_name(parameter)
572
+ parameter.respond_to?(:name) ? parameter.name : "argument"
573
+ end
574
+
575
+ def legacy_directional_argument_expression?(expression)
576
+ return true if expression.is_a?(AST::UnaryOp) && %w[in out inout].include?(expression.operator)
577
+
578
+ if expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && %w[ptr_of ref_of].include?(expression.callee.name)
579
+ return expression.arguments.length == 1
580
+ end
581
+
582
+ cast = pointer_like_cast_expression(expression)
583
+ return false unless cast
584
+
585
+ lvalue_expression?(cast[:source]) || legacy_directional_argument_expression?(cast[:source])
586
+ end
587
+
588
+ def lvalue_expression?(expression)
589
+ expression.is_a?(AST::Identifier) || expression.is_a?(AST::MemberAccess) || expression.is_a?(AST::IndexAccess)
590
+ end
591
+ end
592
+ end
593
+ end