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,1286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class Linter
5
+ module LinterVisitors
6
+ private
7
+
8
+ def visit_source_file(source_file)
9
+ @declared_callable_names = declared_callable_names(source_file)
10
+ @declared_directional_functions = declared_directional_functions(source_file)
11
+ profile_phase("seed_module_bindings") { seed_module_bindings(source_file) }
12
+ profile_phase("rule.unused_imports") { check_unused_imports(source_file) }
13
+ profile_phase("rule.platform_api_drift") { check_platform_api_drift(source_file) } if full_tier?
14
+ source_file.declarations.each do |declaration|
15
+ case declaration
16
+ when AST::FunctionDef
17
+ visit_function(declaration)
18
+ when AST::MethodDef
19
+ warn_reserved_primitive_name(declaration.name, line: declaration.line, column: declaration.column, kind_label: "function")
20
+ visit_function(declaration)
21
+ when AST::ExtendingBlock
22
+ generic_context = extending_block_generic?(declaration)
23
+ declaration.methods.each do |method|
24
+ warn_reserved_primitive_name(method.name, line: method.line, column: method.column, kind_label: "function")
25
+ visit_function(method, generic_context:)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ def seed_module_bindings(source_file)
31
+ @module_bindings = {}
32
+
33
+ import_names = source_file.imports.filter_map do |import|
34
+ import.alias_name || import.path.parts.last
35
+ end
36
+ source_file.imports.each do |import|
37
+ local_name = import.alias_name || import.path.parts.last
38
+ next if ignored_binding_name?(local_name)
39
+
40
+ declare_reserved_import_alias_module_binding(
41
+ local_name,
42
+ kind_label: "import alias",
43
+ line: import.line,
44
+ column: import.column,
45
+ unavailable_names: import_names,
46
+ )
47
+ end
48
+
49
+ source_file.declarations.each do |declaration|
50
+ kind_label = case declaration
51
+ when AST::FunctionDef, AST::ExternFunctionDecl, AST::ForeignFunctionDecl
52
+ "function"
53
+ when AST::ConstDecl
54
+ "constant"
55
+ when AST::VarDecl
56
+ "module variable"
57
+ when AST::EventDecl
58
+ "event"
59
+ else
60
+ nil
61
+ end
62
+ next unless kind_label
63
+ next if ignored_binding_name?(declaration.name)
64
+
65
+ declare_reserved_primitive_module_binding(
66
+ declaration.name,
67
+ kind_label:,
68
+ line: declaration.line,
69
+ column: declaration_column(declaration),
70
+ unavailable_names: @declared_callable_names,
71
+ )
72
+ end
73
+ end
74
+ def extending_block_generic?(declaration)
75
+ declaration.type_name.respond_to?(:arguments) && declaration.type_name.arguments.any?
76
+ end
77
+ def declared_callable_names(source_file)
78
+ source_file.declarations.each_with_object(Set.new) do |declaration, names|
79
+ case declaration
80
+ when AST::FunctionDef, AST::ExternFunctionDecl, AST::ForeignFunctionDecl,
81
+ AST::ConstDecl, AST::VarDecl, AST::EventDecl
82
+ names << declaration.name
83
+ end
84
+ end
85
+ end
86
+ def declared_directional_functions(source_file)
87
+ source_file.declarations.each_with_object({}) do |declaration, functions|
88
+ next unless declaration.is_a?(AST::ExternFunctionDecl) || declaration.is_a?(AST::ForeignFunctionDecl)
89
+ next unless declaration.params.any? { |param| %i[in out inout].include?(param.mode) }
90
+
91
+ functions[declaration.name] = declaration
92
+ end
93
+ end
94
+ def visit_function(function, generic_context: false)
95
+ generic_body = generic_context || function.type_params.any?
96
+ @generic_function_depth += 1 if generic_body
97
+ @current_function_stack << function
98
+ with_scope do
99
+ profile_phase("rule.reserved_primitive_type_params") do
100
+ warn_reserved_primitive_type_params(function.type_params, kind_label: "type parameter")
101
+ end
102
+ profile_phase("declare_params") do
103
+ function.params.each do |param|
104
+ declare_param(
105
+ param.name,
106
+ line: param_line(param, fallback: function.line),
107
+ column: param_column(param)
108
+ )
109
+ end
110
+ end
111
+ profile_phase("visit_statement_list") { visit_statement_list(function.body) }
112
+ if full_tier?
113
+ profile_phase("rule.dead_assignment") { emit_dead_assignment_warnings(function.body) }
114
+ profile_phase("rule.unreachable") { emit_unreachable_warnings(function.body) }
115
+ profile_phase("rule.borrow") { emit_borrow_warnings(function.body) }
116
+ profile_phase("rule.constant_condition") { emit_constant_condition_warnings(function.body) }
117
+ profile_phase("rule.redundant_null_check") { emit_redundant_null_check_warnings(function.body) }
118
+ profile_phase("rule.loop_single_iteration") { emit_loop_single_iteration_warnings(function.body) }
119
+ end
120
+ profile_phase("rule.owning_release") { emit_owning_release_warnings(function) }
121
+ profile_phase("rule.prefer_let_else") { emit_prefer_let_else_warnings(function.body) }
122
+ profile_phase("rule.prefer_var_else") { emit_prefer_var_else_warnings(function.body) }
123
+ profile_phase("rule.redundant_return") { emit_redundant_return_warnings(function) }
124
+ end
125
+ profile_phase("rule.missing_return") { check_missing_return(function) }
126
+ ensure
127
+ @current_function_stack.pop
128
+ @generic_function_depth -= 1 if generic_body
129
+ end
130
+ def generic_function_context?
131
+ @generic_function_depth > 0
132
+ end
133
+ def visit_statement_list(stmts)
134
+ terminated = false
135
+ stmts.each do |stmt|
136
+ next if terminated # skip visitation only; ControlFlow emits the warning
137
+
138
+ visit_statement(stmt)
139
+ terminated = true if terminator?(stmt)
140
+ end
141
+ end
142
+ def terminator?(stmt)
143
+ stmt.is_a?(AST::ReturnStmt) || stmt.is_a?(AST::BreakStmt) || stmt.is_a?(AST::ContinueStmt)
144
+ end
145
+ def visit_statement(statement)
146
+ case statement
147
+ when AST::LocalDecl
148
+ visit_expression(statement.value) if statement.value
149
+ declare_local(
150
+ statement.name,
151
+ statement.line,
152
+ column: statement.column,
153
+ var: statement.kind == :var
154
+ )
155
+ check_redundant_type_annotation(statement)
156
+ flag_redundant_widening_cast(statement.value) if statement.type && statement.value
157
+ record_ptr_candidate(statement)
158
+ when AST::Assignment
159
+ visit_expression(statement.value) # visit RHS first — reads in RHS count against dead-assignment
160
+ mark_assignment_target_reads(statement.target, statement.operator) # compound: marks target as read
161
+ visit_assignment_target(statement.target) # non-identifier sub-expressions in target
162
+ mark_mutated(statement.target)
163
+ check_self_assignment(statement)
164
+ check_noop_compound_assignment(statement)
165
+ flag_redundant_widening_cast(statement.value) if statement.operator == "="
166
+ when AST::IfStmt
167
+ statement.branches.each do |branch|
168
+ visit_expression(branch.condition)
169
+ with_scope { visit_statement_list(branch.body) }
170
+ end
171
+ with_scope { visit_statement_list(statement.else_body) } if statement.else_body
172
+ check_redundant_else(statement)
173
+ check_duplicate_if_conditions(statement)
174
+ if full_tier?
175
+ check_prefer_inline_if(statement)
176
+ check_prefer_conditional_expression_if(statement)
177
+ end
178
+ when AST::MatchStmt
179
+ visit_expression(statement.expression)
180
+ statement.arms.each do |arm|
181
+ with_scope do
182
+ binding_line = arm.binding_line || statement.line
183
+ binding_column = arm.binding_column
184
+ warn_redundant_ignored_match_binding(arm.binding_name, line: binding_line, column: binding_column)
185
+ declare_local(arm.binding_name, binding_line, column: binding_column, var: false) if arm.binding_name
186
+ visit_statement_list(arm.body)
187
+ end
188
+ end
189
+ if full_tier?
190
+ check_prefer_conditional_expression_match(statement)
191
+ check_prefer_or_pattern(statement.arms, body_of: ->(arm) { arm.body })
192
+ check_prefer_try(statement.expression, statement.arms)
193
+ end
194
+ when AST::UnsafeStmt
195
+ @unsafe_depth += 1
196
+ with_scope { visit_statement_list(statement.body) }
197
+ @unsafe_depth -= 1
198
+ when AST::ForStmt
199
+ visit_expression(statement.iterable)
200
+ with_scope do
201
+ declare_local(statement.name, statement.line, column: statement.column, var: false) if statement.name
202
+ visit_statement_list(statement.body)
203
+ end
204
+ when AST::WhileStmt
205
+ visit_expression(statement.condition)
206
+ with_scope { visit_statement_list(statement.body) }
207
+ when AST::ReturnStmt
208
+ visit_expression(statement.value) if statement.value
209
+ flag_redundant_widening_cast(statement.value) if statement.value
210
+ when AST::DeferStmt
211
+ visit_expression(statement.expression) if statement.expression
212
+ with_scope { visit_statement_list(statement.body) } if statement.body
213
+ when AST::ExpressionStmt
214
+ visit_expression(statement.expression)
215
+ check_useless_expression(statement)
216
+ when AST::GatherStmt
217
+ statement.handles.each { |handle| mark_used(handle.name) if handle.is_a?(AST::Identifier) }
218
+ when AST::StaticAssert
219
+ visit_expression(statement.condition)
220
+ when AST::BreakStmt, AST::ContinueStmt
221
+ nil
222
+ when AST::WhenStmt
223
+ visit_expression(statement.discriminant)
224
+ statement.branches.each do |branch|
225
+ with_scope { visit_statement_list(branch.body) }
226
+ end
227
+ with_scope { visit_statement_list(statement.else_body) } if statement.else_body
228
+ when AST::ErrorBlockStmt
229
+ with_scope { visit_statement_list(statement.body) } if statement.body
230
+ else
231
+ nil
232
+ end
233
+ end
234
+ def visit_expression(expression)
235
+ case expression
236
+ when nil
237
+ nil
238
+ when AST::Identifier
239
+ mark_used(expression.name, identifier: expression)
240
+ when AST::MemberAccess
241
+ visit_expression(expression.receiver)
242
+ when AST::IndexAccess
243
+ visit_expression(expression.receiver)
244
+ visit_expression(expression.index)
245
+ when AST::Specialization
246
+ visit_expression(expression.callee)
247
+ expression.arguments.each { |argument| visit_type_argument(argument) }
248
+ when AST::Call
249
+ visit_expression(expression.callee)
250
+ expression.arguments.each do |argument|
251
+ visit_expression(argument.value)
252
+ mark_call_argument_mutated(argument.value)
253
+ end
254
+ mark_alias_source_mutated(expression)
255
+ mark_call_receiver_mutated(expression)
256
+ check_directional_ffi_call(expression)
257
+ check_prefer_struct_with(expression) if full_tier?
258
+ when AST::UnaryOp
259
+ visit_expression(expression.operand)
260
+ when AST::BinaryOp
261
+ visit_expression(expression.left)
262
+ visit_expression(expression.right)
263
+ check_self_comparison(expression)
264
+ check_redundant_bool_compare(expression)
265
+ when AST::RangeExpr
266
+ visit_expression(expression.start_expr)
267
+ visit_expression(expression.end_expr)
268
+ when AST::ExpressionList
269
+ expression.elements.each { |e| visit_expression(e) }
270
+ when AST::IfExpr
271
+ visit_expression(expression.condition)
272
+ visit_expression(expression.then_expression)
273
+ visit_expression(expression.else_expression)
274
+ when AST::MatchExpr
275
+ visit_expression(expression.expression)
276
+ expression.arms.each do |arm|
277
+ with_scope do
278
+ binding_line = arm.binding_line || expression.line
279
+ binding_column = arm.binding_column
280
+ warn_redundant_ignored_match_binding(arm.binding_name, line: binding_line, column: binding_column)
281
+ declare_local(arm.binding_name, binding_line, column: binding_column, var: false) if arm.binding_name
282
+ visit_expression(arm.value)
283
+ end
284
+ end
285
+ if full_tier?
286
+ check_prefer_is_variant(expression)
287
+ check_prefer_or_pattern(expression.arms, body_of: ->(arm) { arm.value })
288
+ end
289
+ when AST::UnsafeExpr
290
+ @unsafe_depth += 1
291
+ visit_expression(expression.expression)
292
+ @unsafe_depth -= 1
293
+ when AST::ProcExpr
294
+ with_scope do
295
+ fallback_line = expression.respond_to?(:line) ? expression.line : nil
296
+ expression.params.each do |param|
297
+ declare_param(
298
+ param.name,
299
+ line: param_line(param, fallback: fallback_line),
300
+ column: param_column(param)
301
+ )
302
+ end
303
+ visit_statement_list(expression.body)
304
+ if full_tier?
305
+ emit_dead_assignment_warnings(expression.body)
306
+ emit_unreachable_warnings(expression.body)
307
+ emit_borrow_warnings(expression.body)
308
+ emit_constant_condition_warnings(expression.body)
309
+ emit_redundant_null_check_warnings(expression.body)
310
+ emit_loop_single_iteration_warnings(expression.body)
311
+ emit_owning_release_warnings(expression.body)
312
+ end
313
+ emit_prefer_let_else_warnings(expression.body)
314
+ emit_prefer_var_else_warnings(expression.body)
315
+ end
316
+ when AST::AwaitExpr
317
+ visit_expression(expression.expression)
318
+ when AST::FormatString
319
+ expression.parts.each do |part|
320
+ next unless part.is_a?(AST::FormatExprPart)
321
+
322
+ visit_expression(part.expression)
323
+ end
324
+ when AST::IntegerLiteral, AST::FloatLiteral, AST::StringLiteral, AST::BooleanLiteral, AST::NullLiteral
325
+ nil
326
+ when AST::SizeofExpr, AST::AlignofExpr
327
+ visit_type_ref(expression.type)
328
+ when AST::OffsetofExpr
329
+ mark_used(expression.field)
330
+ when AST::PrefixCast
331
+ visit_expression(expression.expression)
332
+ check_redundant_cast(expression)
333
+ else
334
+ nil
335
+ end
336
+ end
337
+ def visit_type_argument(argument)
338
+ visit_expression(argument.value) if argument.respond_to?(:value)
339
+ end
340
+ def visit_type_ref(type_ref)
341
+ return unless type_ref
342
+
343
+ type_ref.name.parts.each { |part| mark_used(part) }
344
+ type_ref.arguments.each do |argument|
345
+ next unless argument.respond_to?(:value)
346
+
347
+ visit_type_ref(argument.value) if argument.value.is_a?(AST::TypeRef)
348
+ end
349
+ end
350
+ def visit_assignment_target(target)
351
+ case target
352
+ when AST::Identifier
353
+ nil
354
+ when AST::MemberAccess
355
+ visit_expression(target.receiver)
356
+ when AST::IndexAccess
357
+ visit_expression(target.receiver)
358
+ visit_expression(target.index)
359
+ else
360
+ visit_expression(target)
361
+ end
362
+ end
363
+ def mark_assignment_target_reads(target, operator)
364
+ return if operator == "="
365
+
366
+ mark_used(target.name, identifier: target) if target.is_a?(AST::Identifier)
367
+ end
368
+ def mark_mutated(target)
369
+ return unless target.is_a?(AST::Identifier) || target.is_a?(AST::IndexAccess) || target.is_a?(AST::MemberAccess)
370
+
371
+ # For direct identifier assignment (e.g., x = value), mark x as mutated.
372
+ if target.is_a?(AST::Identifier)
373
+ @scopes.reverse_each do |scope|
374
+ binding = scope[target.name]
375
+ next unless binding
376
+
377
+ binding.mutated = true
378
+ return
379
+ end
380
+ end
381
+
382
+ # For index assignment (e.g., array[0] = value), mark the array as mutated.
383
+ if target.is_a?(AST::IndexAccess)
384
+ mark_mutated(target.receiver)
385
+ end
386
+
387
+ # For field assignment (e.g., rect.w = value), mark the struct variable as mutated.
388
+ if target.is_a?(AST::MemberAccess)
389
+ mark_mutated(target.receiver)
390
+ end
391
+ end
392
+ def mark_call_argument_mutated(expression)
393
+ if expression.is_a?(AST::Identifier) && mutating_argument_identifier?(expression)
394
+ mark_mutated(expression)
395
+ return
396
+ end
397
+
398
+ if expression.is_a?(AST::UnaryOp) && %w[out inout].include?(expression.operator)
399
+ mark_mutated(expression.operand)
400
+ return
401
+ end
402
+
403
+ # ref_of(x) and ptr_of(x) can expose writable aliases — treat as potential
404
+ # mutation since callees may write through them (common C-FFI out-param pattern).
405
+ if expression.is_a?(AST::Call) &&
406
+ expression.callee.is_a?(AST::Identifier) &&
407
+ ["ref_of", "ptr_of"].include?(expression.callee.name) &&
408
+ expression.arguments.length == 1
409
+ mark_mutated(expression.arguments.first.value)
410
+ end
411
+ end
412
+ def mutating_argument_identifier?(expression)
413
+ return false unless expression.is_a?(AST::Identifier)
414
+
415
+ binding_resolution = @sema_facts&.binding_resolution
416
+ return false unless binding_resolution
417
+
418
+ binding_resolution.mutating_argument_identifier_ids&.key?(expression.object_id) ||
419
+ binding_resolution.mutable_lvalue_argument_identifier_ids&.key?(expression.object_id)
420
+ end
421
+ def mark_call_receiver_mutated(expression)
422
+ return unless expression.is_a?(AST::Call)
423
+ return unless expression.callee.is_a?(AST::MemberAccess)
424
+ return unless editable_receiver_expression?(expression.callee.receiver)
425
+
426
+ mark_mutated(expression.callee.receiver)
427
+ end
428
+ def mark_alias_source_mutated(expression)
429
+ return unless expression.is_a?(AST::Call)
430
+ return unless expression.callee.is_a?(AST::Identifier)
431
+ return unless %w[ref_of ptr_of].include?(expression.callee.name)
432
+ return unless expression.arguments.length == 1
433
+
434
+ mark_mutated(expression.arguments.first.value)
435
+ end
436
+ def editable_receiver_expression?(expression)
437
+ return true unless @sema_facts
438
+
439
+ @sema_facts&.binding_resolution&.editable_receiver_expression_ids&.key?(expression.object_id)
440
+ end
441
+ def with_scope
442
+ @scopes << {}
443
+ yield
444
+ ensure
445
+ emit_scope_warnings(@scopes.pop)
446
+ end
447
+ def declare_local(name, line, column: nil, var: false)
448
+ return if ignored_binding_name?(name)
449
+
450
+ resolve_reserved_primitive_name_conflicts!(name)
451
+
452
+ replacement_name = nil
453
+ replacement_base_name = nil
454
+ if RESERVED_VALUE_TYPE_NAMES.include?(name)
455
+ replacement_base_name = suggested_reserved_primitive_name(name, kind_label: "local")
456
+ replacement_name = next_available_reserved_primitive_name(
457
+ replacement_base_name,
458
+ visible_binding_names(excluding_name: name),
459
+ )
460
+ end
461
+
462
+ # shadow: check whether any outer scope already has a binding for this name
463
+ if @scopes.length > 1
464
+ @scopes[0..-2].each do |outer_scope|
465
+ if outer_scope.key?(name)
466
+ @warnings << Warning.new(
467
+ path: @path, line:, column:, length: name.length, code: "shadow",
468
+ message: "local '#{name}' shadows a binding from an outer scope",
469
+ symbol_name: name
470
+ )
471
+ break
472
+ end
473
+ end
474
+ end
475
+
476
+ @scopes.last[name] = Binding.new(
477
+ name:, line:, column:, used: false,
478
+ binding_kind: :local,
479
+ allow_prefer_let: var && !generic_function_context?,
480
+ mutated: false,
481
+ replacement_name:,
482
+ replacement_base_name:,
483
+ )
484
+ register_reserved_primitive_name_fix(@scopes.last[name], kind_label: "local", replacement_name:) if replacement_name
485
+ end
486
+ def declare_param(name, line: nil, column: nil)
487
+ return if ignored_binding_name?(name)
488
+
489
+ resolve_reserved_primitive_name_conflicts!(name)
490
+
491
+ replacement_name = nil
492
+ replacement_base_name = nil
493
+ if RESERVED_VALUE_TYPE_NAMES.include?(name)
494
+ replacement_base_name = suggested_reserved_primitive_name(name, kind_label: "parameter")
495
+ replacement_name = next_available_reserved_primitive_name(
496
+ replacement_base_name,
497
+ visible_binding_names(excluding_name: name),
498
+ )
499
+ end
500
+
501
+ @scopes.last[name] = Binding.new(
502
+ name:, line:, column:, used: false,
503
+ binding_kind: :param,
504
+ allow_prefer_let: false,
505
+ mutated: false,
506
+ replacement_name:,
507
+ replacement_base_name:,
508
+ )
509
+ register_reserved_primitive_name_fix(@scopes.last[name], kind_label: "parameter", replacement_name:) if replacement_name
510
+ end
511
+ def mark_used(name, identifier: nil)
512
+ @scopes.reverse_each do |scope|
513
+ binding = scope[name]
514
+ next unless binding
515
+
516
+ binding.used = true
517
+ track_binding_unsafe_use(name)
518
+ record_reserved_primitive_identifier_use(binding, identifier)
519
+ return
520
+ end
521
+
522
+ binding = @module_bindings[name]
523
+ return unless binding
524
+
525
+ binding.used = true
526
+ record_reserved_primitive_identifier_use(binding, identifier)
527
+ end
528
+
529
+ def track_binding_unsafe_use(name)
530
+ uses = @binding_ptr_unsafe_uses[name]
531
+ if @unsafe_depth > 0
532
+ uses[:unsafe] += 1
533
+ else
534
+ uses[:safe] += 1
535
+ end
536
+ end
537
+ def emit_scope_warnings(scope)
538
+ scope.each_value do |binding|
539
+ if !binding.used
540
+ code = binding.binding_kind == :param ? "unused-param" : "unused-local"
541
+ kind_label = binding.binding_kind == :param ? "parameter" : "local"
542
+ @warnings << Warning.new(
543
+ path: @path,
544
+ line: binding.line,
545
+ column: binding.column,
546
+ length: binding.name.length,
547
+ code: code,
548
+ message: "unused #{kind_label} '#{binding.name}'",
549
+ symbol_name: binding.name,
550
+ )
551
+ else
552
+ if binding.allow_prefer_let && !binding.mutated
553
+ @warnings << Warning.new(
554
+ path: @path,
555
+ line: binding.line,
556
+ column: binding.column,
557
+ length: binding.name.length,
558
+ code: "prefer-let",
559
+ message: "variable '#{binding.name}' is never reassigned, prefer 'let'",
560
+ severity: :hint,
561
+ symbol_name: binding.name
562
+ )
563
+ end
564
+
565
+ uses = @binding_ptr_unsafe_uses[binding.name]
566
+ if binding.binding_kind != :param && uses[:unsafe] > 0 && uses[:safe] == 0 && @ptr_candidates.include?(binding.name)
567
+ @warnings << Warning.new(
568
+ path: @path,
569
+ line: binding.line,
570
+ column: binding.column,
571
+ length: binding.name.length,
572
+ code: "prefer-own-ptr",
573
+ message: "binding '#{binding.name}' is only used inside unsafe — consider converting its type from ptr to own for auto-deref",
574
+ severity: :hint,
575
+ symbol_name: binding.name,
576
+ )
577
+ end
578
+ end
579
+ end
580
+ end
581
+ # ── redundant type annotation ─────────────────────────────────────────────
582
+
583
+ def check_redundant_type_annotation(statement)
584
+ return unless statement.is_a?(AST::LocalDecl)
585
+ return unless statement.kind == :let
586
+ return unless statement.type
587
+ return unless statement.value
588
+
589
+ declared_name = type_ref_name(statement.type)
590
+ return unless declared_name
591
+
592
+ inferred_name = expression_literal_type_name(statement.value)
593
+ return unless inferred_name
594
+ return unless declared_name == inferred_name
595
+
596
+ @warnings << Warning.new(
597
+ path: @path,
598
+ line: statement.line,
599
+ column: statement.column,
600
+ length: statement.name.length,
601
+ code: "redundant-type-annotation",
602
+ message: "type annotation ': #{declared_name}' is redundant, inferred from initializer",
603
+ severity: :hint,
604
+ symbol_name: statement.name,
605
+ )
606
+ end
607
+
608
+ def record_ptr_candidate(statement)
609
+ return unless statement.is_a?(AST::LocalDecl)
610
+ return unless statement.value
611
+
612
+ if statement.type && statement.type.is_a?(AST::TypeRef)
613
+ type_text = type_ref_source(statement.type)
614
+ return unless type_text&.include?("ptr[")
615
+ else
616
+ value = statement.value
617
+ unless heap_alloc_call?(value)
618
+ alloc_name = alloc_wrapper_name(value)
619
+ return unless alloc_name
620
+ end
621
+ end
622
+
623
+ @ptr_candidates.add(statement.name)
624
+ end
625
+
626
+ def heap_alloc_call?(expr)
627
+ return false unless expr.is_a?(AST::Call)
628
+ return false unless expr.callee.is_a?(AST::MemberAccess)
629
+
630
+ %w[must_alloc alloc must_alloc_zeroed must_resize].include?(expr.callee.member)
631
+ end
632
+
633
+ def alloc_wrapper_name(expr)
634
+ return nil unless expr.is_a?(AST::Call)
635
+ return nil unless expr.callee.is_a?(AST::Identifier)
636
+
637
+ name = expr.callee.name
638
+ %w[alloc_expr alloc_stmt alloc_decl].include?(name) ? name : nil
639
+ end
640
+
641
+ def type_ref_source(type_node)
642
+ return nil unless type_node.is_a?(AST::TypeRef)
643
+ return nil if type_node.name.parts.empty?
644
+
645
+ type_node.name.parts.last
646
+ rescue StandardError
647
+ nil
648
+ end
649
+
650
+ def type_ref_name(type_node)
651
+ return nil unless type_node.is_a?(AST::TypeRef)
652
+ return nil if type_node.name.parts.empty?
653
+
654
+ type_node.name.parts.first
655
+ rescue StandardError
656
+ nil
657
+ end
658
+
659
+ # ── redundant cast ─────────────────────────────────────────────────────────
660
+
661
+ # Detects a two-arm match expression that only maps a single variant arm
662
+ # to a boolean and everything else to the opposite boolean, e.g.
663
+ # match token: TokenKind.eof: true; _: false
664
+ # which is exactly what `token is TokenKind.eof` desugars to. Only fires
665
+ # when the scrutinee is a variant (the `is` operator is variant-only), so
666
+ # enum/int/str bool-matches are never misreported.
667
+ def check_prefer_is_variant(match_expr)
668
+ return unless @sema_facts
669
+
670
+ arms = match_expr.arms
671
+ return unless arms.size == 2
672
+
673
+ wildcard_arm, arm_pattern_arm = classify_is_variant_arms(arms)
674
+ return unless wildcard_arm && arm_pattern_arm
675
+
676
+ # The variant arm must be a bare `Type.arm` reference: no payload
677
+ # destructure and no `as` binding, otherwise it is not equivalent to `is`.
678
+ pattern = arm_pattern_arm.pattern
679
+ return unless pattern.is_a?(AST::MemberAccess)
680
+ return unless arm_pattern_arm.binding_name.nil?
681
+
682
+ variant_value = boolean_literal_value(arm_pattern_arm.value)
683
+ wildcard_value = boolean_literal_value(wildcard_arm.value)
684
+ return if variant_value.nil? || wildcard_value.nil?
685
+ return if variant_value == wildcard_value
686
+
687
+ scrutinee_type = resolve_expr_type(match_expr.expression)
688
+ return unless scrutinee_type.is_a?(Types::Variant)
689
+
690
+ arm_text = expr_source_name(pattern)
691
+ suggestion = variant_value ? "expr is #{arm_text}" : "not (expr is #{arm_text})"
692
+
693
+ @warnings << Warning.new(
694
+ path: @path,
695
+ line: match_expr.line,
696
+ column: match_expr.column,
697
+ length: match_expr.length,
698
+ code: "prefer-is-variant",
699
+ message: "prefer `#{suggestion}` over a match that maps one variant arm to a boolean",
700
+ severity: :hint,
701
+ )
702
+ end
703
+
704
+ def classify_is_variant_arms(arms)
705
+ wildcard = arms.find { |arm| wildcard_pattern?(arm.pattern) }
706
+ other = arms.find { |arm| !wildcard_pattern?(arm.pattern) }
707
+ return [nil, nil] unless wildcard && other
708
+
709
+ [wildcard, other]
710
+ end
711
+
712
+ def wildcard_pattern?(pattern)
713
+ pattern.is_a?(AST::Identifier) && pattern.name == "_"
714
+ end
715
+
716
+ def boolean_literal_value(expr)
717
+ expr.is_a?(AST::BooleanLiteral) ? expr.value : nil
718
+ end
719
+
720
+ def expr_source_name(expr)
721
+ case expr
722
+ when AST::Identifier then expr.name
723
+ when AST::MemberAccess then "#{expr_source_name(expr.receiver)}.#{expr.member}"
724
+ else "…"
725
+ end
726
+ end
727
+
728
+ # ── conciseness hints ─────────────────────────────────────────────────
729
+
730
+ def emit_conciseness_hint(code, line:, column:, message:, length: nil)
731
+ @warnings << Warning.new(path: @path, line:, column:, length:, code:, message:, severity: :hint)
732
+ end
733
+
734
+ # Position-insensitive structural fingerprint of an AST node, used to test
735
+ # whether two branch/arm bodies are equivalent regardless of source
736
+ # location.
737
+ def node_fingerprint(node)
738
+ case node
739
+ when ::Data
740
+ skip = %i[line column length else_line else_column binding_line binding_column]
741
+ parts = node.deconstruct_keys(nil).reject { |k, _| skip.include?(k) }
742
+ "#{node.class.name}(#{parts.map { |k, v| "#{k}:#{node_fingerprint(v)}" }.join(",")})"
743
+ when Array
744
+ "[#{node.map { |e| node_fingerprint(e) }.join(",")}]"
745
+ else
746
+ node.inspect
747
+ end
748
+ end
749
+
750
+ def single_statement_body(body)
751
+ body.is_a?(Array) && body.size == 1 ? body.first : nil
752
+ end
753
+
754
+ def inline_simple_statement?(stmt)
755
+ case stmt
756
+ when AST::ReturnStmt, AST::Assignment, AST::ExpressionStmt, AST::BreakStmt, AST::ContinueStmt, AST::LocalDecl
757
+ true
758
+ else
759
+ false
760
+ end
761
+ end
762
+
763
+ # prefer-inline-if: a block-form if/else whose every branch is a single
764
+ # simple statement can be written on one line.
765
+ def check_prefer_inline_if(statement)
766
+ return if statement.inline
767
+ return unless statement.else_body
768
+
769
+ bodies = statement.branches.map(&:body) + [statement.else_body]
770
+ stmts = bodies.map { |b| single_statement_body(b) }
771
+ return if stmts.any?(&:nil?)
772
+ return unless stmts.all? { |s| inline_simple_statement?(s) }
773
+
774
+ emit_conciseness_hint(
775
+ "prefer-inline-if",
776
+ line: statement.line,
777
+ column: statement.branches.first&.column,
778
+ message: "if/else with single-statement branches can be written inline",
779
+ )
780
+ end
781
+
782
+ # prefer-conditional-expression: if/match whose every branch either
783
+ # returns a value or assigns the same lvalue can become an expression form
784
+ # (`return if …: … else: …` or `x = match …`).
785
+ def check_prefer_conditional_expression_if(statement)
786
+ return unless statement.else_body
787
+
788
+ stmts = (statement.branches.map(&:body) + [statement.else_body]).map { |b| single_statement_body(b) }
789
+ report_conditional_expression(stmts, line: statement.line, column: statement.branches.first&.column, kind: "if")
790
+ end
791
+
792
+ def check_prefer_conditional_expression_match(statement)
793
+ return unless statement.arms.any? { |arm| wildcard_pattern?(arm.pattern) }
794
+ return if statement.arms.any? { |arm| arm.binding_name }
795
+
796
+ stmts = statement.arms.map { |arm| single_statement_body(arm.body) }
797
+ report_conditional_expression(stmts, line: statement.line, column: statement.column, kind: "match")
798
+ end
799
+
800
+ def report_conditional_expression(stmts, line:, column:, kind:)
801
+ return if stmts.empty? || stmts.any?(&:nil?)
802
+
803
+ if stmts.all? { |s| s.is_a?(AST::ReturnStmt) && s.value }
804
+ emit_conciseness_hint(
805
+ "prefer-conditional-expression",
806
+ line:, column:,
807
+ message: "every #{kind} branch returns a value; rewrite as `return #{kind} …` expression",
808
+ )
809
+ elsif stmts.all? { |s| s.is_a?(AST::Assignment) && s.operator == "=" } &&
810
+ stmts.map { |s| node_fingerprint(s.target) }.uniq.size == 1
811
+ target_name = stmts.first.target.name
812
+ emit_conciseness_hint(
813
+ "prefer-conditional-expression",
814
+ line:, column:,
815
+ message: "every #{kind} branch assigns to '#{target_name}'; rewrite as `let #{target_name} = #{kind} …` expression",
816
+ )
817
+ end
818
+ end
819
+
820
+ # prefer-or-pattern: adjacent match arms with identical bodies and no
821
+ # bindings can be merged with `|`.
822
+ def check_prefer_or_pattern(arms, body_of:)
823
+ arms.each_cons(2) do |first, second|
824
+ next if first.binding_name || second.binding_name
825
+ next if wildcard_pattern?(first.pattern) || wildcard_pattern?(second.pattern)
826
+ next unless node_fingerprint(body_of.call(first)) == node_fingerprint(body_of.call(second))
827
+
828
+ pattern_line = second.pattern.respond_to?(:line) ? second.pattern.line : nil
829
+ pattern_column = second.pattern.respond_to?(:column) ? second.pattern.column : nil
830
+ emit_conciseness_hint(
831
+ "prefer-or-pattern",
832
+ line: pattern_line,
833
+ column: pattern_column,
834
+ message: "adjacent match arms have identical bodies; merge them with `|`",
835
+ )
836
+ end
837
+ end
838
+
839
+ # prefer-struct-with: a constructor literal that copies all-but-one field
840
+ # from the same source value can use `.with(...)`.
841
+ def check_prefer_struct_with(call)
842
+ return unless @sema_facts
843
+ return unless call.callee.is_a?(AST::Identifier) || call.callee.is_a?(AST::MemberAccess)
844
+ return if call.arguments.empty?
845
+ return unless call.arguments.all? { |arg| arg.name }
846
+
847
+ copied, changed = call.arguments.partition { |arg| copy_field_argument?(arg) }
848
+ return unless changed.size >= 1 && copied.size >= 2
849
+
850
+ sources = copied.map { |arg| node_fingerprint(arg.value.receiver) }.uniq
851
+ return unless sources.size == 1
852
+
853
+ struct_type = resolve_expr_type(call)
854
+ return unless struct_type.is_a?(Types::Struct)
855
+ source_type = resolve_expr_type(copied.first.value.receiver)
856
+ return unless source_type.equal?(struct_type) || (source_type.respond_to?(:name) && source_type.name == struct_type.name)
857
+
858
+ field_names = struct_type.fields.keys.map(&:to_s).to_set
859
+ copied_names = copied.map { |arg| arg.name.to_s }.to_set
860
+ changed_names = changed.map { |arg| arg.name.to_s }
861
+ return unless (field_names - changed_names.to_set) == copied_names
862
+
863
+ source_text = expr_source_name(copied.first.value.receiver)
864
+ emit_conciseness_hint(
865
+ "prefer-struct-with",
866
+ line: call.callee.respond_to?(:line) ? call.callee.line : nil,
867
+ column: call.callee.respond_to?(:column) ? call.callee.column : nil,
868
+ message: "copies #{copied.size} field(s) from `#{source_text}`; use `#{source_text}.with(#{changed_names.join(", ")} = …)`",
869
+ )
870
+ end
871
+
872
+ def copy_field_argument?(argument)
873
+ value = argument.value
874
+ value.is_a?(AST::MemberAccess) && value.member.to_s == argument.name.to_s
875
+ end
876
+
877
+ # prefer-try: a two-arm match on Result/Option whose failure/none arm only
878
+ # returns is error-propagation and can usually be written `expr?`.
879
+ def check_prefer_try(scrutinee, arms)
880
+ return unless @sema_facts
881
+ return unless arms.size == 2
882
+
883
+ scrutinee_type = resolve_expr_type(scrutinee)
884
+ return unless scrutinee_type.is_a?(Types::Variant)
885
+ base = scrutinee_type.name.to_s.split(".").last
886
+ return unless %w[Result Option].include?(base)
887
+
888
+ early_return_arm = arms.find do |arm|
889
+ stmt = single_statement_body(arm.body)
890
+ next false unless stmt.is_a?(AST::ReturnStmt) && stmt.value
891
+ next false unless short_circuit_arm_pattern?(arm.pattern, base)
892
+
893
+ propagation_return?(stmt.value, base, arm.binding_name)
894
+ end
895
+ return unless early_return_arm
896
+
897
+ emit_conciseness_hint(
898
+ "prefer-try",
899
+ line: scrutinee.respond_to?(:line) ? scrutinee.line : nil,
900
+ column: scrutinee.respond_to?(:column) ? scrutinee.column : nil,
901
+ message: "this #{base} match only propagates the failure branch; consider `expr?`",
902
+ )
903
+ end
904
+
905
+ def short_circuit_arm_pattern?(pattern, base)
906
+ name = pattern.is_a?(AST::MemberAccess) ? pattern.member.to_s : nil
907
+ return false unless name
908
+
909
+ (base == "Result" && name == "failure") || (base == "Option" && name == "none")
910
+ end
911
+
912
+ # The early-return value must re-propagate the short-circuit case
913
+ # *unchanged*: `Option[_].none`, or `Result[_, _].failure(error = <b>.error)`
914
+ # where `<b>` is the arm's own binding. This excludes map/map_error
915
+ # patterns (transformed error or value), which `expr?` cannot express.
916
+ def propagation_return?(value, base, binding_name)
917
+ if base == "Option"
918
+ return member_named?(value, "none") || (value.is_a?(AST::Call) && member_named?(value.callee, "none"))
919
+ end
920
+
921
+ return false unless binding_name
922
+ return false unless value.is_a?(AST::Call) && member_named?(value.callee, "failure")
923
+ return false unless value.arguments.size == 1
924
+
925
+ error_arg = value.arguments.first
926
+ return false unless error_arg.name.to_s == "error"
927
+
928
+ forwarded = error_arg.value
929
+ forwarded.is_a?(AST::MemberAccess) &&
930
+ forwarded.member.to_s == "error" &&
931
+ forwarded.receiver.is_a?(AST::Identifier) &&
932
+ forwarded.receiver.name == binding_name
933
+ end
934
+
935
+ def member_named?(node, name)
936
+ node.is_a?(AST::MemberAccess) && node.member.to_s == name
937
+ end
938
+
939
+ def check_redundant_cast(cast_expr)
940
+ return unless cast_expr.is_a?(AST::PrefixCast)
941
+ return unless cast_expr.target_type
942
+
943
+ target_name = type_ref_name(cast_expr.target_type)
944
+ return unless target_name
945
+
946
+ # When facts are available compare the *full* resolved types, not just
947
+ # the nominal base name. Comparing names alone wrongly treats e.g.
948
+ # `ptr[void]<-p` (p: ptr[ubyte]) as same-type because both are "ptr",
949
+ # and removing that cast is a real type error.
950
+ if @sema_facts
951
+ cast_type = resolve_expr_type(cast_expr)
952
+ inner_type = resolve_expr_type(cast_expr.expression)
953
+ if cast_type && inner_type
954
+ emit_redundant_cast(cast_expr, target_name, "cast to same type is redundant") if same_resolved_type?(cast_type, inner_type)
955
+ if own_to_pointer_cast_redundant?(inner_type, cast_type)
956
+ emit_redundant_cast(cast_expr, target_name, "implicit own->ptr coercion makes this cast redundant")
957
+ end
958
+ return
959
+ end
960
+ end
961
+
962
+ # Fallback when the inner type cannot be resolved (e.g. no sema facts):
963
+ # a value literal whose type is known by name.
964
+ literal_name = expression_literal_type_name(cast_expr.expression)
965
+ if literal_name && literal_name == target_name
966
+ emit_redundant_cast(cast_expr, target_name, "cast to same type is redundant")
967
+ end
968
+
969
+ # NOTE: widening redundancy is intentionally NOT reported here. A
970
+ # widening cast can be load-bearing at its site (e.g. `(ulong<-x) << 32`
971
+ # controls the shift width), so it is only redundant at a coercion
972
+ # boundary whose declared type is the target. Those cases are handled by
973
+ # check_boundary_widening_cast at the specific boundary sites.
974
+ end
975
+
976
+ # Reports a widening cast as redundant only when it sits *directly* at a
977
+ # coercion slot: the RHS of a typed `let`/`var`, a `return` value, or the
978
+ # RHS of a plain `=` assignment. At those positions the slot type is
979
+ # pinned by an annotation / return type / declared lvalue, so the value is
980
+ # coerced to that type and never consumed by a width-sensitive operator at
981
+ # the site. Because widening composes losslessly, removing the cast
982
+ # preserves both type and behavior regardless of the exact slot type.
983
+ #
984
+ # Call arguments and aggregate field initializers are intentionally NOT
985
+ # treated as slots: a generic parameter/field would let the removed cast
986
+ # change type inference (and hence behavior), which is not sound without
987
+ # resolving the callee/struct signature.
988
+ def flag_redundant_widening_cast(value)
989
+ return unless value.is_a?(AST::PrefixCast)
990
+ return unless @sema_facts
991
+
992
+ target_name = type_ref_name(value.target_type)
993
+ return unless target_name
994
+
995
+ inner_type = resolve_expr_type(value.expression)
996
+ return unless inner_type.is_a?(Types::Primitive)
997
+ return if inner_type.name == target_name # same-type handled elsewhere
998
+ return unless implicit_cast_allowed?(inner_type, target_name)
999
+
1000
+ emit_redundant_cast(value, target_name, "implicit widening makes this cast redundant")
1001
+ end
1002
+
1003
+ def same_resolved_type?(left, right)
1004
+ return true if left.equal?(right)
1005
+
1006
+ left.to_s == right.to_s
1007
+ end
1008
+
1009
+ def own_to_pointer_cast_redundant?(inner_type, cast_type)
1010
+ return false unless inner_type.is_a?(Types::GenericInstance) && inner_type.name == "own"
1011
+ return false unless cast_type.is_a?(Types::GenericInstance)
1012
+ return false unless %w[ptr const_ptr].include?(cast_type.name)
1013
+ return false unless inner_type.arguments.length == 1 && cast_type.arguments.length == 1
1014
+
1015
+ inner_type.arguments.first.to_s == cast_type.arguments.first.to_s
1016
+ end
1017
+
1018
+ def emit_redundant_cast(cast_expr, target_name, reason)
1019
+ line = cast_expr.respond_to?(:line) ? cast_expr.line : expression_line(cast_expr)
1020
+ column = cast_expr.respond_to?(:column) ? cast_expr.column : expression_column(cast_expr)
1021
+
1022
+ @warnings << Warning.new(
1023
+ path: @path,
1024
+ line: line,
1025
+ column: column,
1026
+ length: expression_length(cast_expr),
1027
+ code: "redundant-cast",
1028
+ message: "#{reason}: #{target_name}<-",
1029
+ severity: :hint,
1030
+ )
1031
+ end
1032
+
1033
+ def implicit_cast_allowed?(inner_type, target_name)
1034
+ return true if inner_type.name == target_name
1035
+
1036
+ target = find_primitive_type(target_name)
1037
+ return false unless target
1038
+ return false unless target.is_a?(Types::Primitive)
1039
+ return false unless inner_type.fixed_width_integer? && target.fixed_width_integer?
1040
+
1041
+ if inner_type.signed_integer? == target.signed_integer?
1042
+ return target.integer_width >= inner_type.integer_width
1043
+ end
1044
+
1045
+ return false if inner_type.signed_integer?
1046
+
1047
+ target.signed_integer? && target.integer_width > inner_type.integer_width
1048
+ end
1049
+
1050
+ def find_primitive_type(name)
1051
+ return nil unless @sema_facts
1052
+ return nil unless @sema_facts.types
1053
+
1054
+ @sema_facts.types[name]
1055
+ end
1056
+
1057
+ def resolve_expr_type(expr)
1058
+ return nil unless @sema_facts
1059
+ return nil unless expr
1060
+
1061
+ node_id = @sema_facts.ast.node_ids[expr.object_id]
1062
+ return nil unless node_id
1063
+
1064
+ @sema_facts.resolved_expr_types[node_id]
1065
+ end
1066
+
1067
+ def expression_literal_type_name(expr)
1068
+ case expr
1069
+ when AST::IntegerLiteral then "int"
1070
+ when AST::StringLiteral then expr.cstring ? "cstr" : "str"
1071
+ when AST::FloatLiteral then "float"
1072
+ when AST::BooleanLiteral then "bool"
1073
+ else nil
1074
+ end
1075
+ end
1076
+ # ── borrow facts helpers ──────────────────────────────────────────────────
1077
+
1078
+ BORROW_CALL_NAMES = %w[ref_of ptr_of].freeze
1079
+
1080
+ def collect_borrowed_names(stmts)
1081
+ names = Set.new
1082
+ stmts.each { |s| collect_borrows_from_stmt(s, names) }
1083
+ names
1084
+ end
1085
+
1086
+ def collect_borrows_from_stmt(stmt, names)
1087
+ case stmt
1088
+ when AST::LocalDecl
1089
+ collect_borrows_from_expr(stmt.value, names) if stmt.value
1090
+ when AST::Assignment
1091
+ collect_borrows_from_expr(stmt.value, names)
1092
+ when AST::ExpressionStmt
1093
+ collect_borrows_from_expr(stmt.expression, names)
1094
+ when AST::IfStmt
1095
+ stmt.branches.each do |b|
1096
+ collect_borrows_from_expr(b.condition, names)
1097
+ b.body.each { |s| collect_borrows_from_stmt(s, names) }
1098
+ end
1099
+ stmt.else_body&.each { |s| collect_borrows_from_stmt(s, names) }
1100
+ when AST::WhileStmt
1101
+ collect_borrows_from_expr(stmt.condition, names)
1102
+ stmt.body.each { |s| collect_borrows_from_stmt(s, names) }
1103
+ when AST::ForStmt
1104
+ stmt.body.each { |s| collect_borrows_from_stmt(s, names) }
1105
+ when AST::ReturnStmt
1106
+ collect_borrows_from_expr(stmt.value, names) if stmt.value
1107
+ when AST::MatchStmt
1108
+ collect_borrows_from_expr(stmt.expression, names)
1109
+ stmt.arms.each { |arm| arm.body.each { |s| collect_borrows_from_stmt(s, names) } }
1110
+ when AST::UnsafeStmt
1111
+ stmt.body.each { |s| collect_borrows_from_stmt(s, names) }
1112
+ when AST::DeferStmt
1113
+ collect_borrows_from_expr(stmt.expression, names) if stmt.expression
1114
+ stmt.body&.each { |s| collect_borrows_from_stmt(s, names) }
1115
+ when AST::WhenStmt
1116
+ collect_borrows_from_expr(stmt.discriminant, names)
1117
+ stmt.branches.each { |b| b.body.each { |s| collect_borrows_from_stmt(s, names) } }
1118
+ stmt.else_body&.each { |s| collect_borrows_from_stmt(s, names) }
1119
+ end
1120
+ end
1121
+
1122
+ def collect_borrows_from_expr(expr, names, inside_call_argument: false)
1123
+ case expr
1124
+ when nil then nil
1125
+ when AST::Call
1126
+ if !inside_call_argument && expr.callee.is_a?(AST::Identifier) && BORROW_CALL_NAMES.include?(expr.callee.name)
1127
+ arg = expr.arguments.first
1128
+ if arg&.value.is_a?(AST::Identifier)
1129
+ names << arg.value.name
1130
+ end
1131
+ else
1132
+ collect_borrows_from_expr(expr.callee, names)
1133
+ expr.arguments.each { |a| collect_borrows_from_expr(a.value, names, inside_call_argument: true) }
1134
+ end
1135
+ when AST::UnaryOp then collect_borrows_from_expr(expr.operand, names, inside_call_argument:)
1136
+ when AST::BinaryOp
1137
+ collect_borrows_from_expr(expr.left, names, inside_call_argument:)
1138
+ collect_borrows_from_expr(expr.right, names, inside_call_argument:)
1139
+ when AST::IfExpr
1140
+ collect_borrows_from_expr(expr.condition, names, inside_call_argument:)
1141
+ collect_borrows_from_expr(expr.then_expression, names, inside_call_argument:)
1142
+ collect_borrows_from_expr(expr.else_expression, names, inside_call_argument:)
1143
+ when AST::AwaitExpr
1144
+ collect_borrows_from_expr(expr.expression, names, inside_call_argument:)
1145
+ when AST::UnsafeExpr
1146
+ collect_borrows_from_expr(expr.expression, names, inside_call_argument:)
1147
+ when AST::MemberAccess then collect_borrows_from_expr(expr.receiver, names, inside_call_argument:)
1148
+ when AST::IndexAccess
1149
+ collect_borrows_from_expr(expr.receiver, names, inside_call_argument:)
1150
+ collect_borrows_from_expr(expr.index, names, inside_call_argument:)
1151
+ end
1152
+ end
1153
+
1154
+ def collect_written_names(stmts)
1155
+ names = Set.new
1156
+ stmts.each { |s| collect_writes_from_stmt(s, names) }
1157
+ names
1158
+ end
1159
+
1160
+ def collect_writes_from_stmt(stmt, names)
1161
+ case stmt
1162
+ when AST::Assignment
1163
+ names << stmt.target.name if stmt.target.is_a?(AST::Identifier)
1164
+ collect_writes_from_stmt_list(stmt_sub_stmts(stmt), names)
1165
+ when AST::IfStmt
1166
+ stmt.branches.each { |b| b.body.each { |s| collect_writes_from_stmt(s, names) } }
1167
+ stmt.else_body&.each { |s| collect_writes_from_stmt(s, names) }
1168
+ when AST::WhileStmt
1169
+ stmt.body.each { |s| collect_writes_from_stmt(s, names) }
1170
+ when AST::ForStmt
1171
+ stmt.body.each { |s| collect_writes_from_stmt(s, names) }
1172
+ when AST::MatchStmt
1173
+ stmt.arms.each { |arm| arm.body.each { |s| collect_writes_from_stmt(s, names) } }
1174
+ when AST::WhenStmt
1175
+ stmt.branches.each { |b| b.body.each { |s| collect_writes_from_stmt(s, names) } }
1176
+ stmt.else_body&.each { |s| collect_writes_from_stmt(s, names) }
1177
+ when AST::UnsafeStmt
1178
+ stmt.body.each { |s| collect_writes_from_stmt(s, names) }
1179
+ when AST::DeferStmt
1180
+ stmt.body&.each { |s| collect_writes_from_stmt(s, names) }
1181
+ end
1182
+ end
1183
+
1184
+ def collect_writes_from_stmt_list(stmts, names)
1185
+ stmts.each { |s| collect_writes_from_stmt(s, names) }
1186
+ end
1187
+
1188
+ def stmt_sub_stmts(_stmt)
1189
+ []
1190
+ end
1191
+
1192
+ def find_borrow_location(stmts, name)
1193
+ stmts.each do |stmt|
1194
+ location = find_borrow_location_in_stmt(stmt, name)
1195
+ return location if location
1196
+ end
1197
+ nil
1198
+ end
1199
+
1200
+ def find_borrow_location_in_stmt(stmt, name)
1201
+ case stmt
1202
+ when AST::LocalDecl
1203
+ find_borrow_location_in_expr(stmt.value, name) if stmt.value
1204
+ when AST::Assignment
1205
+ find_borrow_location_in_expr(stmt.value, name)
1206
+ when AST::ExpressionStmt
1207
+ find_borrow_location_in_expr(stmt.expression, name)
1208
+ when AST::IfStmt
1209
+ stmt.branches.each do |b|
1210
+ location = find_borrow_location_in_expr(b.condition, name)
1211
+ return location if location
1212
+ b.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1213
+ end
1214
+ stmt.else_body&.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1215
+ nil
1216
+ when AST::WhileStmt
1217
+ location = find_borrow_location_in_expr(stmt.condition, name)
1218
+ return location if location
1219
+ stmt.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1220
+ nil
1221
+ when AST::ForStmt
1222
+ stmt.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1223
+ nil
1224
+ when AST::ReturnStmt
1225
+ find_borrow_location_in_expr(stmt.value, name) if stmt.value
1226
+ when AST::MatchStmt
1227
+ location = find_borrow_location_in_expr(stmt.expression, name)
1228
+ return location if location
1229
+ stmt.arms.each do |arm|
1230
+ arm.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1231
+ end
1232
+ nil
1233
+ when AST::UnsafeStmt
1234
+ stmt.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1235
+ nil
1236
+ when AST::DeferStmt
1237
+ location = find_borrow_location_in_expr(stmt.expression, name) if stmt.expression
1238
+ return location if location
1239
+ stmt.body&.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1240
+ nil
1241
+ when AST::WhenStmt
1242
+ location = find_borrow_location_in_expr(stmt.discriminant, name)
1243
+ return location if location
1244
+ stmt.branches.each do |b|
1245
+ b.body.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1246
+ end
1247
+ stmt.else_body&.each { |s| location = find_borrow_location_in_stmt(s, name); return location if location }
1248
+ nil
1249
+ end
1250
+ end
1251
+
1252
+ def find_borrow_location_in_expr(expr, name)
1253
+ case expr
1254
+ when nil then nil
1255
+ when AST::Call
1256
+ if expr.callee.is_a?(AST::Identifier) && BORROW_CALL_NAMES.include?(expr.callee.name)
1257
+ arg = expr.arguments.first
1258
+ if arg&.value.is_a?(AST::Identifier) && arg.value.name == name
1259
+ return [arg.value.line, arg.value.column, arg.value.name.length]
1260
+ end
1261
+ end
1262
+ location = find_borrow_location_in_expr(expr.callee, name)
1263
+ return location if location
1264
+ expr.arguments.each do |a|
1265
+ location = find_borrow_location_in_expr(a.value, name)
1266
+ return location if location
1267
+ end
1268
+ nil
1269
+ when AST::UnaryOp then find_borrow_location_in_expr(expr.operand, name)
1270
+ when AST::BinaryOp
1271
+ find_borrow_location_in_expr(expr.left, name) || find_borrow_location_in_expr(expr.right, name)
1272
+ when AST::IfExpr
1273
+ find_borrow_location_in_expr(expr.condition, name) ||
1274
+ find_borrow_location_in_expr(expr.then_expression, name) ||
1275
+ find_borrow_location_in_expr(expr.else_expression, name)
1276
+ when AST::AwaitExpr
1277
+ find_borrow_location_in_expr(expr.expression, name)
1278
+ when AST::UnsafeExpr
1279
+ find_borrow_location_in_expr(expr.expression, name)
1280
+ else
1281
+ nil
1282
+ end
1283
+ end
1284
+ end
1285
+ end
1286
+ end