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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/AUTHORS +3 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +21 -0
- data/README.md +1208 -0
- data/Rakefile +332 -0
- data/bin/mtc +8 -0
- data/bin/profile-mtc-checks +133 -0
- data/bin/tracy-profiler +0 -0
- data/docs/build-guide.md +536 -0
- data/docs/index.html +2778 -0
- data/docs/language-design.md +1519 -0
- data/docs/language-manual.md +1534 -0
- data/lib/milk_tea/base.rb +49 -0
- data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
- data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
- data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
- data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
- data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
- data/lib/milk_tea/bindings/bindgen.rb +207 -0
- data/lib/milk_tea/bindings/cli.rb +121 -0
- data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
- data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
- data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
- data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
- data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
- data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
- data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
- data/lib/milk_tea/bindings/steamworks.rb +629 -0
- data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
- data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
- data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
- data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
- data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
- data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
- data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
- data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
- data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
- data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
- data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
- data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
- data/lib/milk_tea/bindings.rb +23 -0
- data/lib/milk_tea/core/ast.rb +311 -0
- data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
- data/lib/milk_tea/core/binding_types.rb +18 -0
- data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
- data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
- data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
- data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
- data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
- data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
- data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
- data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
- data/lib/milk_tea/core/c_backend/statements.rb +563 -0
- data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
- data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
- data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
- data/lib/milk_tea/core/c_backend.rb +287 -0
- data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
- data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
- data/lib/milk_tea/core/compile_time.rb +329 -0
- data/lib/milk_tea/core/control_flow/builder.rb +582 -0
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
- data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
- data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
- data/lib/milk_tea/core/control_flow/graph.rb +90 -0
- data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
- data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
- data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
- data/lib/milk_tea/core/control_flow/termination.rb +31 -0
- data/lib/milk_tea/core/control_flow.rb +34 -0
- data/lib/milk_tea/core/cst.rb +48 -0
- data/lib/milk_tea/core/cst_builder.rb +19 -0
- data/lib/milk_tea/core/ir.rb +85 -0
- data/lib/milk_tea/core/keywords.rb +97 -0
- data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
- data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
- data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
- data/lib/milk_tea/core/lexer/indentation.rb +62 -0
- data/lib/milk_tea/core/lexer/numbers.rb +96 -0
- data/lib/milk_tea/core/lexer/recovery.rb +32 -0
- data/lib/milk_tea/core/lexer/strings.rb +167 -0
- data/lib/milk_tea/core/lexer/symbols.rb +71 -0
- data/lib/milk_tea/core/lexer/trivia.rb +53 -0
- data/lib/milk_tea/core/lexer.rb +430 -0
- data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
- data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
- data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
- data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
- data/lib/milk_tea/core/lowering/async.rb +714 -0
- data/lib/milk_tea/core/lowering/block.rb +1052 -0
- data/lib/milk_tea/core/lowering/calls.rb +1565 -0
- data/lib/milk_tea/core/lowering/declarations.rb +214 -0
- data/lib/milk_tea/core/lowering/dyn.rb +206 -0
- data/lib/milk_tea/core/lowering/events.rb +1054 -0
- data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
- data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
- data/lib/milk_tea/core/lowering/functions.rb +242 -0
- data/lib/milk_tea/core/lowering/loops.rb +1087 -0
- data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
- data/lib/milk_tea/core/lowering/proc.rb +419 -0
- data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
- data/lib/milk_tea/core/lowering/scans.rb +220 -0
- data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
- data/lib/milk_tea/core/lowering/utils.rb +1453 -0
- data/lib/milk_tea/core/lowering.rb +378 -0
- data/lib/milk_tea/core/module_binder.rb +181 -0
- data/lib/milk_tea/core/module_loader/errors.rb +21 -0
- data/lib/milk_tea/core/module_loader.rb +479 -0
- data/lib/milk_tea/core/module_path_resolver.rb +153 -0
- data/lib/milk_tea/core/module_roots.rb +88 -0
- data/lib/milk_tea/core/parser/attributes.rb +71 -0
- data/lib/milk_tea/core/parser/blocks.rb +119 -0
- data/lib/milk_tea/core/parser/declarations.rb +749 -0
- data/lib/milk_tea/core/parser/expressions.rb +624 -0
- data/lib/milk_tea/core/parser/recovery.rb +131 -0
- data/lib/milk_tea/core/parser/statements.rb +756 -0
- data/lib/milk_tea/core/parser/types.rb +271 -0
- data/lib/milk_tea/core/parser.rb +400 -0
- data/lib/milk_tea/core/prelude_installer.rb +29 -0
- data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
- data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
- data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
- data/lib/milk_tea/core/pretty_printer.rb +17 -0
- data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
- data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
- data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
- data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
- data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
- data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
- data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
- data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
- data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
- data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
- data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
- data/lib/milk_tea/core/token.rb +26 -0
- data/lib/milk_tea/core/token_stream.rb +30 -0
- data/lib/milk_tea/core/types/layout.rb +243 -0
- data/lib/milk_tea/core/types/predicates.rb +609 -0
- data/lib/milk_tea/core/types/registry.rb +83 -0
- data/lib/milk_tea/core/types/types.rb +1696 -0
- data/lib/milk_tea/core/types/visitor.rb +442 -0
- data/lib/milk_tea/core.rb +25 -0
- data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
- data/lib/milk_tea/dap/protocol.rb +58 -0
- data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
- data/lib/milk_tea/dap/server/debug_map.rb +291 -0
- data/lib/milk_tea/dap/server/handlers.rb +374 -0
- data/lib/milk_tea/dap/server/launch.rb +261 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
- data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
- data/lib/milk_tea/dap/server/utilities.rb +160 -0
- data/lib/milk_tea/dap/server/wire.rb +55 -0
- data/lib/milk_tea/dap/server.rb +131 -0
- data/lib/milk_tea/dap/session.rb +152 -0
- data/lib/milk_tea/dap.rb +6 -0
- data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
- data/lib/milk_tea/lsp/diagnostics.rb +611 -0
- data/lib/milk_tea/lsp/protocol.rb +104 -0
- data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
- data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
- data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
- data/lib/milk_tea/lsp/server/completion.rb +1099 -0
- data/lib/milk_tea/lsp/server/configuration.rb +167 -0
- data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
- data/lib/milk_tea/lsp/server/definition.rb +779 -0
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
- data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
- data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
- data/lib/milk_tea/lsp/server/formatting.rb +575 -0
- data/lib/milk_tea/lsp/server/hover.rb +1465 -0
- data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
- data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
- data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
- data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
- data/lib/milk_tea/lsp/server/progress.rb +47 -0
- data/lib/milk_tea/lsp/server/references.rb +433 -0
- data/lib/milk_tea/lsp/server/rename.rb +598 -0
- data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
- data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
- data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
- data/lib/milk_tea/lsp/server/utilities.rb +520 -0
- data/lib/milk_tea/lsp/server.rb +415 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
- data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
- data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
- data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
- data/lib/milk_tea/lsp/workspace/store.rb +154 -0
- data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
- data/lib/milk_tea/lsp/workspace.rb +127 -0
- data/lib/milk_tea/lsp.rb +13 -0
- data/lib/milk_tea/packages/atomic_write.rb +63 -0
- data/lib/milk_tea/packages/dependency_solver.rb +194 -0
- data/lib/milk_tea/packages/graph.rb +139 -0
- data/lib/milk_tea/packages/lock.rb +421 -0
- data/lib/milk_tea/packages/manager_cli.rb +485 -0
- data/lib/milk_tea/packages/manifest.rb +356 -0
- data/lib/milk_tea/packages/manifest_editor.rb +134 -0
- data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
- data/lib/milk_tea/packages/registry_store.rb +420 -0
- data/lib/milk_tea/packages/services.rb +41 -0
- data/lib/milk_tea/packages/source_cache.rb +71 -0
- data/lib/milk_tea/packages/source_fetcher.rb +124 -0
- data/lib/milk_tea/packages/source_resolver.rb +389 -0
- data/lib/milk_tea/packages/version.rb +164 -0
- data/lib/milk_tea/packages.rb +16 -0
- data/lib/milk_tea/tooling/asset_pack.rb +141 -0
- data/lib/milk_tea/tooling/build.rb +1124 -0
- data/lib/milk_tea/tooling/build_cache.rb +240 -0
- data/lib/milk_tea/tooling/cli.rb +2688 -0
- data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
- data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
- data/lib/milk_tea/tooling/debug_map.rb +248 -0
- data/lib/milk_tea/tooling/docs_app.rb +369 -0
- data/lib/milk_tea/tooling/error_formatter.rb +86 -0
- data/lib/milk_tea/tooling/formatter.rb +787 -0
- data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
- data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
- data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
- data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
- data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
- data/lib/milk_tea/tooling/linter/rules.rb +593 -0
- data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
- data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
- data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
- data/lib/milk_tea/tooling/linter.rb +798 -0
- data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
- data/lib/milk_tea/tooling/public/css/docs.css +166 -0
- data/lib/milk_tea/tooling/public/js/docs.js +94 -0
- data/lib/milk_tea/tooling/run.rb +408 -0
- data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
- data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
- data/lib/milk_tea/tooling/views/404.erb +7 -0
- data/lib/milk_tea/tooling/views/index.erb +87 -0
- data/lib/milk_tea/tooling/views/layout.erb +69 -0
- data/lib/milk_tea/tooling/views/module.erb +97 -0
- data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
- data/lib/milk_tea/tooling.rb +20 -0
- data/lib/milk_tea.rb +8 -0
- 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
|