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,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class Linter
|
|
5
|
+
module LinterFlowRules
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def emit_dead_assignment_warnings(stmts)
|
|
9
|
+
analysis = dead_assignment_analysis(stmts)
|
|
10
|
+
return unless analysis
|
|
11
|
+
|
|
12
|
+
graph = analysis.graph
|
|
13
|
+
liveness = analysis.liveness
|
|
14
|
+
readable_bindings = analysis.readable_bindings
|
|
15
|
+
locally_declared = analysis.locally_declared
|
|
16
|
+
|
|
17
|
+
graph.each_node do |node|
|
|
18
|
+
node.writes_info.each do |write|
|
|
19
|
+
next if write[:origin] == :call_argument
|
|
20
|
+
next if write[:origin] == :declaration
|
|
21
|
+
|
|
22
|
+
binding_key = write[:binding_key]
|
|
23
|
+
name = write[:name]
|
|
24
|
+
next unless readable_bindings.include?(binding_key)
|
|
25
|
+
next unless locally_declared.include?(binding_key)
|
|
26
|
+
next if liveness.live_out[node.id].include?(binding_key)
|
|
27
|
+
|
|
28
|
+
@warnings << Warning.new(
|
|
29
|
+
path: @path,
|
|
30
|
+
line: write[:line],
|
|
31
|
+
column: write[:column],
|
|
32
|
+
length: name.length,
|
|
33
|
+
code: "dead-assignment",
|
|
34
|
+
message: "value assigned to '#{name}' is never read",
|
|
35
|
+
symbol_name: name
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
def emit_unreachable_warnings(stmts)
|
|
41
|
+
analysis = statement_flow_analysis(stmts)
|
|
42
|
+
return unless analysis
|
|
43
|
+
|
|
44
|
+
graph = analysis.graph
|
|
45
|
+
reachable = analysis.reachability
|
|
46
|
+
|
|
47
|
+
graph.each_node do |node|
|
|
48
|
+
next if reachable.reachable_ids.include?(node.id)
|
|
49
|
+
next if node.kind == :exit
|
|
50
|
+
next unless node.statement
|
|
51
|
+
|
|
52
|
+
statement = node.statement
|
|
53
|
+
line = statement.respond_to?(:line) ? statement.line : nil
|
|
54
|
+
@warnings << Warning.new(
|
|
55
|
+
path: @path,
|
|
56
|
+
line:,
|
|
57
|
+
column: statement_column(statement),
|
|
58
|
+
length: statement_length(statement),
|
|
59
|
+
code: "unreachable-code",
|
|
60
|
+
message: "unreachable code"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
# Detects obvious aliasing hazards: a mutable reference (ref_of / ptr_of)
|
|
65
|
+
# is taken from a local variable that is also written later in the same body.
|
|
66
|
+
def emit_borrow_warnings(stmts)
|
|
67
|
+
return if stmts.nil? || stmts.empty?
|
|
68
|
+
|
|
69
|
+
borrowed = collect_borrowed_names(stmts)
|
|
70
|
+
return if borrowed.empty?
|
|
71
|
+
|
|
72
|
+
written = collect_written_names(stmts)
|
|
73
|
+
(borrowed & written).each do |name|
|
|
74
|
+
# Find the earliest borrow site for the warning location
|
|
75
|
+
borrow_line, borrow_column, borrow_length = find_borrow_location(stmts, name)
|
|
76
|
+
@warnings << Warning.new(
|
|
77
|
+
path: @path,
|
|
78
|
+
line: borrow_line,
|
|
79
|
+
column: borrow_column,
|
|
80
|
+
length: borrow_length,
|
|
81
|
+
code: "borrow-and-mutate",
|
|
82
|
+
message: "'#{name}' is borrowed via ref_of/ptr_of and also mutated in the same scope — potential aliasing hazard",
|
|
83
|
+
severity: :warning,
|
|
84
|
+
symbol_name: name,
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
def cfg_binding_resolution
|
|
89
|
+
return @cfg_binding_resolution if @cfg_binding_resolution_computed
|
|
90
|
+
|
|
91
|
+
binding_resolution = @sema_facts&.binding_resolution
|
|
92
|
+
@cfg_binding_resolution = if binding_resolution
|
|
93
|
+
ControlFlow::BindingResolution.new(
|
|
94
|
+
identifier_binding_ids: binding_resolution.identifier_binding_ids,
|
|
95
|
+
declaration_binding_ids: binding_resolution.declaration_binding_ids,
|
|
96
|
+
mutating_argument_identifier_ids: binding_resolution.mutating_argument_identifier_ids,
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
@cfg_binding_resolution_computed = true
|
|
100
|
+
@cfg_binding_resolution
|
|
101
|
+
end
|
|
102
|
+
def statement_flow_analysis(stmts)
|
|
103
|
+
return nil if stmts.nil? || stmts.empty?
|
|
104
|
+
|
|
105
|
+
@statement_flow_analysis_cache[stmts.object_id] ||= begin
|
|
106
|
+
binding_resolution = cfg_binding_resolution
|
|
107
|
+
graph = profile_phase("flow.graph") do
|
|
108
|
+
ControlFlow::Builder.new(ignore_name: method(:ignored_binding_name?), binding_resolution:).build(stmts)
|
|
109
|
+
end
|
|
110
|
+
reachability = profile_phase("flow.reachability") { ControlFlow::Reachability.solve(graph) }
|
|
111
|
+
nullability = profile_phase("flow.nullability") { ControlFlow::NullabilityFlow.solve(graph) }
|
|
112
|
+
constant_propagation = profile_phase("flow.constant_propagation") do
|
|
113
|
+
ControlFlow::ConstantPropagation.solve(graph, binding_resolution:, strict_binding_ids: !binding_resolution.nil?)
|
|
114
|
+
end
|
|
115
|
+
loop_body_nodes = profile_phase("flow.loop_body_nodes") { compute_loop_body_nodes(graph) }
|
|
116
|
+
StatementFlowAnalysis.new(graph:, reachability:, nullability:, constant_propagation:, loop_body_nodes:)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
def dead_assignment_analysis(stmts)
|
|
120
|
+
return nil if stmts.nil? || stmts.empty?
|
|
121
|
+
|
|
122
|
+
@dead_assignment_analysis_cache[stmts.object_id] ||= begin
|
|
123
|
+
binding_resolution = cfg_binding_resolution
|
|
124
|
+
graph = profile_phase("dead_assignment.graph") do
|
|
125
|
+
ControlFlow::Builder.new(
|
|
126
|
+
ignore_name: method(:ignored_binding_name?),
|
|
127
|
+
binding_resolution:,
|
|
128
|
+
local_decl_without_initializer_writes: true,
|
|
129
|
+
).build(stmts)
|
|
130
|
+
end
|
|
131
|
+
liveness = profile_phase("dead_assignment.liveness") { ControlFlow::Liveness.solve(graph) }
|
|
132
|
+
locally_declared = profile_phase("dead_assignment.locals") do
|
|
133
|
+
graph.each_node.each_with_object(Set.new) do |node, bindings|
|
|
134
|
+
node.writes_info.each do |write|
|
|
135
|
+
bindings << write[:binding_key] if write[:origin] == :declaration
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
DeadAssignmentAnalysis.new(
|
|
140
|
+
graph:,
|
|
141
|
+
liveness:,
|
|
142
|
+
readable_bindings: graph.read_bindings,
|
|
143
|
+
locally_declared:,
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
def cfg_identifier_binding_key(identifier)
|
|
148
|
+
binding_resolution = cfg_binding_resolution
|
|
149
|
+
return identifier.name unless binding_resolution
|
|
150
|
+
|
|
151
|
+
binding_resolution.identifier_binding_ids[identifier.object_id] || identifier.name
|
|
152
|
+
end
|
|
153
|
+
def ignored_binding_name?(name)
|
|
154
|
+
return true unless name
|
|
155
|
+
|
|
156
|
+
name == "_" || name.start_with?("_")
|
|
157
|
+
end
|
|
158
|
+
# ── constant-condition ─────────────────────────────────────────────────
|
|
159
|
+
# Uses ConstantPropagation to detect conditions that are always true/false.
|
|
160
|
+
# Skips `while true` — it is an idiomatic infinite loop.
|
|
161
|
+
# Skips if conditions inside loops, since variables can change across iterations.
|
|
162
|
+
|
|
163
|
+
def emit_constant_condition_warnings(stmts)
|
|
164
|
+
return if stmts.nil? || stmts.empty?
|
|
165
|
+
|
|
166
|
+
analysis = statement_flow_analysis(stmts)
|
|
167
|
+
return unless analysis
|
|
168
|
+
|
|
169
|
+
binding_resolution = cfg_binding_resolution
|
|
170
|
+
graph = analysis.graph
|
|
171
|
+
cp = analysis.constant_propagation
|
|
172
|
+
loop_bodies = analysis.loop_body_nodes
|
|
173
|
+
|
|
174
|
+
graph.each_node do |node|
|
|
175
|
+
cond_expr, line, keyword_pattern, skip_node =
|
|
176
|
+
case node.kind
|
|
177
|
+
when :if_condition
|
|
178
|
+
branch = node.statement
|
|
179
|
+
# Skip if conditions inside loops; variables can change across iterations.
|
|
180
|
+
skip = loop_bodies.include?(node.id)
|
|
181
|
+
[branch&.condition, branch&.line || node.line, "else if|if", skip]
|
|
182
|
+
when :while_condition
|
|
183
|
+
wstmt = node.statement
|
|
184
|
+
# `while true` is an idiomatic infinite loop — do not warn
|
|
185
|
+
skip = wstmt&.condition.is_a?(AST::BooleanLiteral) && wstmt.condition.value == true
|
|
186
|
+
condition = wstmt&.condition
|
|
187
|
+
[condition, node.line, "while", skip]
|
|
188
|
+
else
|
|
189
|
+
next
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
next if skip_node || cond_expr.nil?
|
|
193
|
+
|
|
194
|
+
in_state = cp.in_states[node.id] || {}
|
|
195
|
+
const_val = ControlFlow::ConstantPropagation.constant_value_of(
|
|
196
|
+
cond_expr,
|
|
197
|
+
in_state,
|
|
198
|
+
binding_resolution:,
|
|
199
|
+
strict_binding_ids: !binding_resolution.nil?
|
|
200
|
+
)
|
|
201
|
+
next unless const_val == true || const_val == false
|
|
202
|
+
|
|
203
|
+
ctx = node.kind == :while_condition ? "loop condition" : "branch condition"
|
|
204
|
+
line, column, length = condition_span(cond_expr, line:, keyword_pattern:)
|
|
205
|
+
@warnings << Warning.new(
|
|
206
|
+
path: @path,
|
|
207
|
+
line:,
|
|
208
|
+
column:,
|
|
209
|
+
length:,
|
|
210
|
+
code: "constant-condition",
|
|
211
|
+
message: "#{ctx} is always #{const_val}",
|
|
212
|
+
severity: :warning,
|
|
213
|
+
symbol_name: condition_symbol_name(cond_expr)
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
# Returns the set of node IDs that are inside loops (reachable from a back-edge).
|
|
218
|
+
# A back-edge exists when a node is reachable from its own successors.
|
|
219
|
+
private def compute_loop_body_nodes(graph)
|
|
220
|
+
loop_nodes = Set.new
|
|
221
|
+
|
|
222
|
+
# Find all back-edges: detect cycles by checking if any successor of a node
|
|
223
|
+
# can reach back to that node.
|
|
224
|
+
graph.each_node do |node|
|
|
225
|
+
node.succs.each do |succ_id|
|
|
226
|
+
# Check if succ can reach back to node (indicating a loop/cycle).
|
|
227
|
+
if reachable_from?(graph, succ_id, node.id)
|
|
228
|
+
# Mark all nodes reachable from succ (the loop body) as inside a loop.
|
|
229
|
+
mark_reachable_nodes(graph, succ_id, loop_nodes)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
loop_nodes
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Returns true if target_id is reachable from start_id via forward edges.
|
|
238
|
+
private def reachable_from?(graph, start_id, target_id)
|
|
239
|
+
visited = Set.new
|
|
240
|
+
queue = [start_id]
|
|
241
|
+
|
|
242
|
+
while queue.any?
|
|
243
|
+
node_id = queue.shift
|
|
244
|
+
return true if node_id == target_id
|
|
245
|
+
next if visited.include?(node_id)
|
|
246
|
+
|
|
247
|
+
visited.add(node_id)
|
|
248
|
+
node = graph.nodes[node_id]
|
|
249
|
+
node.succs.each { |succ| queue.push(succ) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
false
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Mark all nodes reachable from start_id as being inside a loop.
|
|
256
|
+
private def mark_reachable_nodes(graph, start_id, loop_nodes)
|
|
257
|
+
visited = Set.new
|
|
258
|
+
queue = [start_id]
|
|
259
|
+
|
|
260
|
+
while queue.any?
|
|
261
|
+
node_id = queue.shift
|
|
262
|
+
loop_nodes.add(node_id)
|
|
263
|
+
next if visited.include?(node_id)
|
|
264
|
+
|
|
265
|
+
visited.add(node_id)
|
|
266
|
+
node = graph.nodes[node_id]
|
|
267
|
+
node.succs.each { |succ| queue.push(succ) unless visited.include?(succ) }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
# ── redundant-null-check ───────────────────────────────────────────────
|
|
271
|
+
# After a variable has been narrowed to non-null by a prior check,
|
|
272
|
+
# a subsequent `x != nil` guard is always true.
|
|
273
|
+
|
|
274
|
+
def emit_redundant_null_check_warnings(stmts)
|
|
275
|
+
return if stmts.nil? || stmts.empty?
|
|
276
|
+
|
|
277
|
+
analysis = statement_flow_analysis(stmts)
|
|
278
|
+
return unless analysis
|
|
279
|
+
|
|
280
|
+
graph = analysis.graph
|
|
281
|
+
nf = analysis.nullability
|
|
282
|
+
|
|
283
|
+
graph.each_node do |node|
|
|
284
|
+
next unless node.kind == :if_condition
|
|
285
|
+
|
|
286
|
+
branch = node.statement
|
|
287
|
+
next unless branch.is_a?(AST::IfBranch)
|
|
288
|
+
|
|
289
|
+
identifier = null_check_identifier(branch.condition)
|
|
290
|
+
next unless identifier
|
|
291
|
+
next if ignored_binding_name?(identifier.name)
|
|
292
|
+
|
|
293
|
+
nonnull = nf.nonnull_before(branch)
|
|
294
|
+
next unless nonnull.include?(cfg_identifier_binding_key(identifier))
|
|
295
|
+
|
|
296
|
+
line, column, length = condition_span(branch.condition, line: node.line, keyword_pattern: "else if|if")
|
|
297
|
+
|
|
298
|
+
@warnings << Warning.new(
|
|
299
|
+
path: @path,
|
|
300
|
+
line:,
|
|
301
|
+
column:,
|
|
302
|
+
length:,
|
|
303
|
+
code: "redundant-null-check",
|
|
304
|
+
message: "'#{identifier.name}' is already known to be non-null here — this nil check is redundant",
|
|
305
|
+
severity: :hint,
|
|
306
|
+
symbol_name: identifier.name
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Returns the Identifier being nil-tested if `cond` is `x != nil` or
|
|
312
|
+
# `nil != x`, otherwise nil.
|
|
313
|
+
def null_check_identifier(cond)
|
|
314
|
+
return nil unless cond.is_a?(AST::BinaryOp) && cond.operator == "!="
|
|
315
|
+
|
|
316
|
+
if cond.left.is_a?(AST::Identifier) && cond.right.is_a?(AST::NullLiteral)
|
|
317
|
+
cond.left
|
|
318
|
+
elsif cond.left.is_a?(AST::NullLiteral) && cond.right.is_a?(AST::Identifier)
|
|
319
|
+
cond.right
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
# ── loop-single-iteration ──────────────────────────────────────────────
|
|
323
|
+
# A loop whose body unconditionally exits (return/break) before the
|
|
324
|
+
# back-edge is taken will execute at most once.
|
|
325
|
+
|
|
326
|
+
def emit_loop_single_iteration_warnings(stmts)
|
|
327
|
+
return if stmts.nil? || stmts.empty?
|
|
328
|
+
|
|
329
|
+
walk_stmts_for_loop_check(stmts)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def walk_stmts_for_loop_check(stmts)
|
|
333
|
+
stmts.each do |stmt|
|
|
334
|
+
case stmt
|
|
335
|
+
when AST::WhileStmt
|
|
336
|
+
body = stmt.body || []
|
|
337
|
+
if !body.empty? && ControlFlow::Termination.loop_body_always_exits?(body)
|
|
338
|
+
@warnings << Warning.new(
|
|
339
|
+
path: @path,
|
|
340
|
+
line: stmt.line,
|
|
341
|
+
column: stmt.column,
|
|
342
|
+
length: stmt.length || "while".length,
|
|
343
|
+
code: "loop-single-iteration",
|
|
344
|
+
message: "loop body always exits on the first iteration - consider replacing with an 'if' block",
|
|
345
|
+
severity: :warning
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
walk_stmts_for_loop_check(body)
|
|
349
|
+
when AST::ForStmt
|
|
350
|
+
body = stmt.body || []
|
|
351
|
+
if !body.empty? && ControlFlow::Termination.loop_body_always_exits?(body)
|
|
352
|
+
@warnings << Warning.new(
|
|
353
|
+
path: @path,
|
|
354
|
+
line: stmt.line,
|
|
355
|
+
column: stmt.column,
|
|
356
|
+
length: (stmt.respond_to?(:length) ? stmt.length : nil) || "for".length,
|
|
357
|
+
code: "loop-single-iteration",
|
|
358
|
+
message: "loop body always exits on the first iteration - consider iterating directly without a loop",
|
|
359
|
+
severity: :warning
|
|
360
|
+
)
|
|
361
|
+
end
|
|
362
|
+
walk_stmts_for_loop_check(body)
|
|
363
|
+
when AST::IfStmt
|
|
364
|
+
stmt.branches.each { |b| walk_stmts_for_loop_check(b.body) }
|
|
365
|
+
walk_stmts_for_loop_check(stmt.else_body) if stmt.else_body
|
|
366
|
+
when AST::MatchStmt
|
|
367
|
+
stmt.arms.each { |arm| walk_stmts_for_loop_check(arm.body) }
|
|
368
|
+
when AST::UnsafeStmt
|
|
369
|
+
walk_stmts_for_loop_check(stmt.body) if stmt.body
|
|
370
|
+
when AST::DeferStmt
|
|
371
|
+
walk_stmts_for_loop_check(stmt.body) if stmt.body
|
|
372
|
+
when AST::WhenStmt
|
|
373
|
+
stmt.branches.each { |b| walk_stmts_for_loop_check(b.body) }
|
|
374
|
+
walk_stmts_for_loop_check(stmt.else_body) if stmt.else_body
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|