mt-lang 0.4.24 → 0.4.25
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 +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -8
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/control_flow/builder.rb +12 -0
- data/lib/milk_tea/tooling/cli/commands/std.rb +84 -0
- data/lib/milk_tea/tooling/cli.rb +25 -0
- data/lib/milk_tea/tooling/linter/fix_engine.rb +100 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +69 -11
- data/lib/milk_tea/tooling/linter/visitors.rb +58 -31
- data/lib/milk_tea/tooling/linter.rb +8 -1
- data/lib/milk_tea/tooling/std_catalog.rb +277 -0
- data/lib/milk_tea/tooling.rb +1 -0
- data/std/async/libuv_runtime.mt +20 -23
- data/std/async/mailbox.mt +2 -2
- data/std/base64.mt +3 -6
- data/std/behavior_tree.mt +1 -6
- data/std/binary.mt +2 -5
- data/std/color.mt +20 -20
- data/std/cookie.mt +1 -4
- data/std/deque.mt +2 -8
- data/std/encoding.mt +1 -4
- data/std/fmt.mt +1 -4
- data/std/graph.mt +5 -1
- data/std/htn.mt +9 -1
- data/std/http/server.mt +1 -4
- data/std/http.mt +14 -17
- data/std/jobs.mt +9 -10
- data/std/json.mt +17 -28
- data/std/linked_map.mt +3 -9
- data/std/map.mt +1 -4
- data/std/net/channel.mt +6 -6
- data/std/net/lobby.mt +3 -3
- data/std/net/manager.mt +0 -1
- data/std/net/mux.mt +1 -1
- data/std/net/packet.mt +4 -1
- data/std/net/punch.mt +3 -2
- data/std/net/rpc.mt +1 -3
- data/std/net/session.mt +12 -25
- data/std/net/sync.mt +3 -3
- data/std/net/turn.mt +7 -3
- data/std/net.mt +145 -159
- data/std/noise.mt +11 -7
- data/std/ordered_map.mt +2 -8
- data/std/ordered_set.mt +3 -5
- data/std/path.mt +1 -4
- data/std/process.mt +7 -18
- data/std/random.mt +2 -2
- data/std/sdl3/runtime.mt +1 -1
- data/std/steering.mt +27 -4
- data/std/string.mt +1 -4
- data/std/tar.mt +8 -10
- data/std/terminal.mt +21 -40
- data/std/thread.mt +3 -4
- data/std/tls.mt +22 -29
- data/std/toml.mt +23 -31
- data/std/uri.mt +2 -1
- data/std/utility.mt +2 -2
- data/std/vec.mt +7 -17
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4c65f190209c5486b52d39b2ac868b2e2c076ad3ec01206151df16675319cf9
|
|
4
|
+
data.tar.gz: 28bb0127501319fb64f0da085409bb8658b6fd9cd41d8c18c0ac2175608f93dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '098dc1e2c0303bbf39dcd4402d7f21482c682f83b904799c1c85a9a73207b4c8443c0a98bdee192db9d17a0bf33ec4a96ad31c03a733f28c57880b7601c91d42'
|
|
7
|
+
data.tar.gz: ce0f5ba1ae8e8c6f958ab4cd5e2d0e89c522a9f814d8e5e31a1e8b8b849faafe437caaab098bd3a4d59755cdbf4f12c23d521810e9892c6e039acf27176b7d10
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.6
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -5,7 +5,7 @@ GEM
|
|
|
5
5
|
drb (2.2.3)
|
|
6
6
|
fiddle (1.1.8)
|
|
7
7
|
logger (1.7.0)
|
|
8
|
-
minitest (6.0.
|
|
8
|
+
minitest (6.0.6)
|
|
9
9
|
drb (~> 2.0)
|
|
10
10
|
prism (~> 1.5)
|
|
11
11
|
minitest-coverage (1.0.0.b3)
|
|
@@ -15,7 +15,7 @@ GEM
|
|
|
15
15
|
puma (6.6.1)
|
|
16
16
|
nio4r (~> 2.0)
|
|
17
17
|
racc (1.8.1)
|
|
18
|
-
rack (3.2.
|
|
18
|
+
rack (3.2.7)
|
|
19
19
|
rack-protection (4.2.1)
|
|
20
20
|
base64 (>= 0.1.0)
|
|
21
21
|
logger (>= 1.6.0)
|
|
@@ -34,7 +34,7 @@ GEM
|
|
|
34
34
|
rack-protection (= 4.2.1)
|
|
35
35
|
rack-session (>= 2.0.0, < 3)
|
|
36
36
|
tilt (~> 2.0)
|
|
37
|
-
tilt (2.
|
|
37
|
+
tilt (2.9.0)
|
|
38
38
|
tomlrb (2.0.4)
|
|
39
39
|
|
|
40
40
|
PLATFORMS
|
|
@@ -58,25 +58,25 @@ CHECKSUMS
|
|
|
58
58
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
59
59
|
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
|
|
60
60
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
61
|
-
minitest (6.0.
|
|
61
|
+
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
|
62
62
|
minitest-coverage (1.0.0.b3) sha256=27c8b6b8df1259325dd206baa2b143c594f8a88b7dfd97fd8d3389a0e8a6ad26
|
|
63
63
|
mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
|
|
64
64
|
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
|
65
65
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
66
66
|
puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e
|
|
67
67
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
68
|
-
rack (3.2.
|
|
68
|
+
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
|
|
69
69
|
rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
|
|
70
70
|
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
|
|
71
71
|
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
|
72
72
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
73
73
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
74
74
|
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
|
|
75
|
-
tilt (2.
|
|
75
|
+
tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5
|
|
76
76
|
tomlrb (2.0.4) sha256=262f77947ac3ac9b3366a0a5940ecd238300c553e2e14f22009e2afcd2181b99
|
|
77
77
|
|
|
78
78
|
RUBY VERSION
|
|
79
|
-
ruby 4.0.
|
|
79
|
+
ruby 4.0.6
|
|
80
80
|
|
|
81
81
|
BUNDLED WITH
|
|
82
|
-
4.0.
|
|
82
|
+
4.0.16
|
data/lib/milk_tea/base.rb
CHANGED
|
@@ -352,6 +352,12 @@ module MilkTea
|
|
|
352
352
|
read_identifiers(expression.expression, names, reads_info)
|
|
353
353
|
when AST::UnsafeExpr
|
|
354
354
|
read_identifiers(expression.expression, names, reads_info)
|
|
355
|
+
when AST::PrefixCast
|
|
356
|
+
read_identifiers(expression.expression, names, reads_info)
|
|
357
|
+
when AST::DetachExpr
|
|
358
|
+
expression.body.each do |stmt|
|
|
359
|
+
read_identifiers(stmt.expression, names, reads_info) if stmt.is_a?(AST::ExpressionStmt)
|
|
360
|
+
end
|
|
355
361
|
when AST::FormatString
|
|
356
362
|
expression.parts.each do |part|
|
|
357
363
|
read_identifiers(part.expression, names, reads_info) if part.is_a?(AST::FormatExprPart)
|
|
@@ -433,6 +439,12 @@ module MilkTea
|
|
|
433
439
|
end
|
|
434
440
|
when AST::UnsafeExpr
|
|
435
441
|
write_targets_from_expression(expression.expression, line:, writes:, writes_info:)
|
|
442
|
+
when AST::PrefixCast
|
|
443
|
+
write_targets_from_expression(expression.expression, line:, writes:, writes_info:)
|
|
444
|
+
when AST::DetachExpr
|
|
445
|
+
expression.body.each do |stmt|
|
|
446
|
+
write_targets_from_expression(stmt.expression, line:, writes:, writes_info:) if stmt.is_a?(AST::ExpressionStmt)
|
|
447
|
+
end
|
|
436
448
|
when AST::FormatString
|
|
437
449
|
expression.parts.each do |part|
|
|
438
450
|
write_targets_from_expression(part.expression, line:, writes:, writes_info:) if part.is_a?(AST::FormatExprPart)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandStd
|
|
6
|
+
def std_command
|
|
7
|
+
subcommand = @argv.shift
|
|
8
|
+
|
|
9
|
+
case subcommand
|
|
10
|
+
when "list"
|
|
11
|
+
std_list_command
|
|
12
|
+
when "show"
|
|
13
|
+
std_show_command
|
|
14
|
+
when nil
|
|
15
|
+
@err.puts("missing std subcommand (expected list or show)")
|
|
16
|
+
print_command_help("std", @err)
|
|
17
|
+
1
|
|
18
|
+
else
|
|
19
|
+
@err.puts("unknown std subcommand #{subcommand}")
|
|
20
|
+
print_command_help("std", @err)
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def std_list_command
|
|
26
|
+
as_json = false
|
|
27
|
+
until @argv.empty?
|
|
28
|
+
option = @argv.shift
|
|
29
|
+
case option
|
|
30
|
+
when "--json"
|
|
31
|
+
as_json = true
|
|
32
|
+
else
|
|
33
|
+
@err.puts("unknown std list option #{option}")
|
|
34
|
+
return 1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
entries = StdCatalog.entries
|
|
39
|
+
if as_json
|
|
40
|
+
@out.puts(JSON.generate(entries.map(&:to_h)))
|
|
41
|
+
return 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
print_std_list(entries)
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print_std_list(entries)
|
|
49
|
+
name_width = entries.map { |entry| entry.name.length }.max
|
|
50
|
+
description_width = entries.map { |entry| entry.description&.length.to_i }.max
|
|
51
|
+
|
|
52
|
+
StdCatalog::CATEGORY_ORDER.each do |category|
|
|
53
|
+
category_entries = entries.select { |entry| entry.category == category }
|
|
54
|
+
next if category_entries.empty?
|
|
55
|
+
|
|
56
|
+
@out.puts("#{category}:")
|
|
57
|
+
category_entries.each do |entry|
|
|
58
|
+
description = entry.description || "(no description)"
|
|
59
|
+
@out.puts(" #{entry.name.ljust(name_width)} #{description.ljust(description_width)} #{entry.path}")
|
|
60
|
+
end
|
|
61
|
+
@out.puts
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def std_show_command
|
|
66
|
+
name = @argv.shift
|
|
67
|
+
unless name
|
|
68
|
+
@err.puts("missing module name")
|
|
69
|
+
print_command_help("std", @err)
|
|
70
|
+
return 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
path = StdCatalog.resolve(name)
|
|
74
|
+
unless path
|
|
75
|
+
@err.puts("std module not found: #{name}")
|
|
76
|
+
return 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@out.write(File.read(path))
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/milk_tea/tooling/cli.rb
CHANGED
|
@@ -33,6 +33,7 @@ module MilkTea
|
|
|
33
33
|
["bindgen", "Generate a binding module from a C header"],
|
|
34
34
|
["cache", "Inspect and manage the build cache"],
|
|
35
35
|
["docs", "Serve the local documentation site"],
|
|
36
|
+
["std", "List standard library modules and print their source"],
|
|
36
37
|
["snapshot", "Render a highlighted HTML snapshot"],
|
|
37
38
|
["lsp", "Start the Language Server Protocol server"],
|
|
38
39
|
["dap", "Start the Debug Adapter Protocol server"],
|
|
@@ -127,6 +128,8 @@ module MilkTea
|
|
|
127
128
|
cache_command
|
|
128
129
|
when "docs"
|
|
129
130
|
docs_command
|
|
131
|
+
when "std"
|
|
132
|
+
std_command
|
|
130
133
|
when "snapshot"
|
|
131
134
|
snapshot_command
|
|
132
135
|
when "lsp"
|
|
@@ -164,6 +167,7 @@ module MilkTea
|
|
|
164
167
|
require_relative "cli/commands/bindgen"
|
|
165
168
|
require_relative "cli/commands/cache"
|
|
166
169
|
require_relative "cli/commands/docs"
|
|
170
|
+
require_relative "cli/commands/std"
|
|
167
171
|
require_relative "cli/commands/snapshot"
|
|
168
172
|
require_relative "cli/commands/lsp"
|
|
169
173
|
require_relative "cli/commands/dap"
|
|
@@ -187,6 +191,7 @@ module MilkTea
|
|
|
187
191
|
include CommandBindgen
|
|
188
192
|
include CommandCache
|
|
189
193
|
include CommandDocs
|
|
194
|
+
include CommandStd
|
|
190
195
|
include CommandSnapshot
|
|
191
196
|
include CommandLsp
|
|
192
197
|
include CommandDap
|
|
@@ -796,6 +801,24 @@ module MilkTea
|
|
|
796
801
|
--open, -o Open the docs in your default browser.
|
|
797
802
|
--port, -p PORT Listen on a specific port (default: random).
|
|
798
803
|
HELP
|
|
804
|
+
"std" => <<~HELP,
|
|
805
|
+
Usage: mtc std list [--json]
|
|
806
|
+
mtc std show MODULE
|
|
807
|
+
|
|
808
|
+
Inspect the Milk Tea standard library shipped in std/.
|
|
809
|
+
|
|
810
|
+
list Print every hand-written standard library module with a short
|
|
811
|
+
description and its path on disk, grouped by category.
|
|
812
|
+
Generated binding modules (std/c/* and imported-binding
|
|
813
|
+
wrappers such as raylib or zstd) are not listed.
|
|
814
|
+
show Print the source of a module. Accepts dotted or slashed names
|
|
815
|
+
(mem.arena, mem/arena) and resolves platform variants such as
|
|
816
|
+
fs to the active platform file. Generated modules stay
|
|
817
|
+
viewable here (for example c.cjson or raylib).
|
|
818
|
+
|
|
819
|
+
Options:
|
|
820
|
+
--json Emit list output as JSON.
|
|
821
|
+
HELP
|
|
799
822
|
"snapshot" => <<~HELP,
|
|
800
823
|
Usage: mtc snapshot INPUT.mt [OPTIONS]
|
|
801
824
|
|
|
@@ -891,6 +914,8 @@ module MilkTea
|
|
|
891
914
|
io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
|
|
892
915
|
io.puts(" mtc cache purge|status")
|
|
893
916
|
io.puts(" mtc docs [--open] [--port PORT]")
|
|
917
|
+
io.puts(" mtc std list [--json]")
|
|
918
|
+
io.puts(" mtc std show MODULE")
|
|
894
919
|
io.puts(" mtc snapshot INPUT.mt [--theme PATH] [-o OUTPUT]")
|
|
895
920
|
io.puts(" mtc lsp [--log-level LEVEL] [--stdio]")
|
|
896
921
|
io.puts(" mtc dap [--backend KIND] [--adapter-path PATH]")
|
|
@@ -17,6 +17,7 @@ module MilkTea
|
|
|
17
17
|
when "redundant-else" then redundant_else_edits(lines, warning)
|
|
18
18
|
when "redundant-return" then redundant_return_edits(lines, warning)
|
|
19
19
|
when "redundant-type-annotation" then redundant_type_annotation_edits(lines, warning)
|
|
20
|
+
when "redundant-unsafe" then redundant_unsafe_edits(lines, warning)
|
|
20
21
|
when "prefer-inline-methods" then prefer_inline_methods_edits(lines, warning)
|
|
21
22
|
when "unused-import" then unused_import_edits(lines, warning)
|
|
22
23
|
when "trailing-list-comma" then trailing_list_comma_edits(lines, warning)
|
|
@@ -302,6 +303,105 @@ module MilkTea
|
|
|
302
303
|
edits
|
|
303
304
|
end
|
|
304
305
|
|
|
306
|
+
# ── redundant-unsafe ───────────────────────────────────────────────────
|
|
307
|
+
# Removes an `unsafe:` wrapper that contains no unsafe operations.
|
|
308
|
+
#
|
|
309
|
+
# Supported shapes:
|
|
310
|
+
# <indent>unsafe:\n<deeper body> (block statement; body is dedented)
|
|
311
|
+
# ... unsafe: expr ... (inline expression on a single line)
|
|
312
|
+
def self.redundant_unsafe_edits(lines, warning)
|
|
313
|
+
return [] unless warning.line
|
|
314
|
+
|
|
315
|
+
line_idx = warning.line - 1
|
|
316
|
+
line = lines[line_idx]
|
|
317
|
+
return [] unless line
|
|
318
|
+
|
|
319
|
+
unsafe_idx = line.index("unsafe:")
|
|
320
|
+
return [] unless unsafe_idx
|
|
321
|
+
|
|
322
|
+
keyword_end = unsafe_idx + "unsafe:".length
|
|
323
|
+
rest = line[keyword_end..]
|
|
324
|
+
|
|
325
|
+
if rest && !rest.strip.empty?
|
|
326
|
+
# Inline expression form: drop the keyword and the whitespace after
|
|
327
|
+
# it, but only when the remainder is balanced on this line.
|
|
328
|
+
remainder = rest.sub(/\A\s+/, "")
|
|
329
|
+
return [] if remainder.empty? || !balanced_line?(remainder)
|
|
330
|
+
|
|
331
|
+
return [FixEdit.new(
|
|
332
|
+
start_line: line_idx,
|
|
333
|
+
start_char: unsafe_idx,
|
|
334
|
+
end_line: line_idx,
|
|
335
|
+
end_char: keyword_end + (rest.length - remainder.length),
|
|
336
|
+
new_text: "",
|
|
337
|
+
)]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Block statement form: `unsafe:` alone, body indented deeper.
|
|
341
|
+
indent = line[0...unsafe_idx]
|
|
342
|
+
return [] unless indent.match?(/\A *\z/)
|
|
343
|
+
|
|
344
|
+
body_start = line_idx + 1
|
|
345
|
+
body_indent = nil
|
|
346
|
+
last_body = nil
|
|
347
|
+
(body_start...lines.length).each do |i|
|
|
348
|
+
candidate = lines[i]
|
|
349
|
+
if candidate.nil? || candidate.strip.empty?
|
|
350
|
+
last_body = i
|
|
351
|
+
next
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
break unless candidate.start_with?(indent + " ")
|
|
355
|
+
|
|
356
|
+
body_indent ||= candidate[/\A */].length - indent.length
|
|
357
|
+
last_body = i
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
return [] if last_body.nil? || last_body < body_start
|
|
361
|
+
return [] if body_indent.nil? || body_indent <= 0
|
|
362
|
+
return [] unless (body_start..last_body).all? do |i|
|
|
363
|
+
l = lines[i]
|
|
364
|
+
l.nil? || l.strip.empty? || l.start_with?(indent + " " * body_indent)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
replacement = (body_start..last_body).map do |i|
|
|
368
|
+
l = lines[i]
|
|
369
|
+
l.nil? || l.strip.empty? ? "" : l[body_indent..].to_s.chomp
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if last_body + 1 <= lines.length - 1
|
|
373
|
+
# Consume through the first following line; apply_fix_edits re-emits
|
|
374
|
+
# it as the suffix of a multi-line replacement.
|
|
375
|
+
end_line = last_body + 1
|
|
376
|
+
end_char = 0
|
|
377
|
+
else
|
|
378
|
+
end_line = last_body
|
|
379
|
+
end_char = lines[last_body].to_s.length
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
[FixEdit.new(
|
|
383
|
+
start_line: line_idx,
|
|
384
|
+
start_char: 0,
|
|
385
|
+
end_line:,
|
|
386
|
+
end_char:,
|
|
387
|
+
new_text: replacement.join("\n") + "\n",
|
|
388
|
+
)]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def self.balanced_line?(text)
|
|
392
|
+
depth = 0
|
|
393
|
+
text.each_char do |char|
|
|
394
|
+
case char
|
|
395
|
+
when "(", "["
|
|
396
|
+
depth += 1
|
|
397
|
+
when ")", "]"
|
|
398
|
+
depth -= 1
|
|
399
|
+
return false if depth.negative?
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
depth.zero?
|
|
403
|
+
end
|
|
404
|
+
|
|
305
405
|
def self.match_paren(line, open_pos)
|
|
306
406
|
depth = 0
|
|
307
407
|
(open_pos...line.length).each do |i|
|
|
@@ -258,10 +258,28 @@ module MilkTea
|
|
|
258
258
|
case expr
|
|
259
259
|
when AST::Call
|
|
260
260
|
expr.arguments.any? do |arg|
|
|
261
|
-
next unless arg.is_a?(AST::Argument)
|
|
261
|
+
next false unless arg.is_a?(AST::Argument)
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
own_transfer_identifier?(arg.value, name)
|
|
264
|
+
end
|
|
265
|
+
when AST::PrefixCast
|
|
266
|
+
own_transfer_identifier?(expr.expression, name)
|
|
267
|
+
else
|
|
268
|
+
false
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def own_transfer_identifier?(value, name)
|
|
273
|
+
case value
|
|
274
|
+
when AST::Identifier
|
|
275
|
+
value.name == name
|
|
276
|
+
when AST::PrefixCast
|
|
277
|
+
own_transfer_identifier?(value.expression, name)
|
|
278
|
+
when AST::Call
|
|
279
|
+
value.arguments.any? do |arg|
|
|
280
|
+
next false unless arg.is_a?(AST::Argument)
|
|
281
|
+
|
|
282
|
+
own_transfer_identifier?(arg.value, name)
|
|
265
283
|
end
|
|
266
284
|
else
|
|
267
285
|
false
|
|
@@ -426,15 +444,29 @@ module MilkTea
|
|
|
426
444
|
return false unless expr.is_a?(AST::Call)
|
|
427
445
|
|
|
428
446
|
expr.arguments.any? do |arg|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
447
|
+
named = arg.is_a?(AST::Argument) && !arg.name.nil?
|
|
448
|
+
argument_transfers?(arg.value, name, named)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# An identifier passed as a named argument transfers ownership (struct
|
|
453
|
+
# field initialization), even when wrapped in a pointer cast
|
|
454
|
+
# (e.g. handle_set_data(h, ptr[void]<-state)). Nested calls are walked
|
|
455
|
+
# so `queue.push(Foo(field = x))` recognizes the inner transfer.
|
|
456
|
+
def argument_transfers?(value, name, named)
|
|
457
|
+
case value
|
|
458
|
+
when AST::Identifier
|
|
459
|
+
named && value.name == name
|
|
460
|
+
when AST::PrefixCast
|
|
461
|
+
inner = value.expression
|
|
462
|
+
inner.is_a?(AST::Identifier) && inner.name == name
|
|
463
|
+
when AST::Call
|
|
464
|
+
value.arguments.any? do |arg|
|
|
465
|
+
nested_named = arg.is_a?(AST::Argument) && !arg.name.nil?
|
|
466
|
+
argument_transfers?(arg.value, name, nested_named)
|
|
437
467
|
end
|
|
468
|
+
else
|
|
469
|
+
false
|
|
438
470
|
end
|
|
439
471
|
end
|
|
440
472
|
|
|
@@ -481,6 +513,9 @@ module MilkTea
|
|
|
481
513
|
return if expr.nil?
|
|
482
514
|
|
|
483
515
|
result << expr.callee.receiver.name if release_call_on_binding?(expr)
|
|
516
|
+
if expr.is_a?(AST::Call) && expr.callee.is_a?(AST::Identifier) && expr.callee.name.start_with?("release_")
|
|
517
|
+
result.merge(release_helper_binding_names(expr))
|
|
518
|
+
end
|
|
484
519
|
|
|
485
520
|
case expr
|
|
486
521
|
when AST::Call
|
|
@@ -496,6 +531,26 @@ module MilkTea
|
|
|
496
531
|
end
|
|
497
532
|
end
|
|
498
533
|
|
|
534
|
+
# std convention: `release_*` helper functions take ownership of the
|
|
535
|
+
# bindings handed to them via ref_of/ptr_of, e.g.
|
|
536
|
+
# `release_string_values(ref_of(values))`. Treat those bindings as
|
|
537
|
+
# released so the leak rule understands the cleanup-helper idiom.
|
|
538
|
+
def release_helper_binding_names(expr)
|
|
539
|
+
names = Set.new
|
|
540
|
+
expr.arguments.each do |arg|
|
|
541
|
+
value = arg.respond_to?(:value) ? arg.value : nil
|
|
542
|
+
next unless value.is_a?(AST::Call)
|
|
543
|
+
next unless value.callee.is_a?(AST::Identifier)
|
|
544
|
+
next unless %w[ref_of ptr_of].include?(value.callee.name)
|
|
545
|
+
|
|
546
|
+
value.arguments.each do |inner|
|
|
547
|
+
binding_name = inner.value
|
|
548
|
+
names << binding_name.name if binding_name.is_a?(AST::Identifier)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
names
|
|
552
|
+
end
|
|
553
|
+
|
|
499
554
|
def _any_stmt?(stmts, &pred)
|
|
500
555
|
return false if stmts.nil?
|
|
501
556
|
|
|
@@ -620,6 +675,9 @@ module MilkTea
|
|
|
620
675
|
return false unless expr
|
|
621
676
|
|
|
622
677
|
return true if release_call_on_binding?(expr) && expr.callee.receiver.name == name
|
|
678
|
+
return true if expr.is_a?(AST::Call) && expr.callee.is_a?(AST::Identifier) &&
|
|
679
|
+
expr.callee.name.start_with?("release_") &&
|
|
680
|
+
release_helper_binding_names(expr).include?(name)
|
|
623
681
|
|
|
624
682
|
case expr
|
|
625
683
|
when AST::Call
|
|
@@ -366,6 +366,10 @@ module MilkTea
|
|
|
366
366
|
end
|
|
367
367
|
def check_redundant_unsafe(node)
|
|
368
368
|
return unless @sema_facts
|
|
369
|
+
# Generic bodies are only fully checked once instantiated elsewhere,
|
|
370
|
+
# so required-unsafe facts are incomplete for them: an unsafe block in
|
|
371
|
+
# a generic body may be required by concrete instances.
|
|
372
|
+
return if generic_function_context?
|
|
369
373
|
return if @sema_facts.required_unsafe_lines.include?(node.line)
|
|
370
374
|
|
|
371
375
|
@warnings << Warning.new(
|
|
@@ -830,43 +834,64 @@ module MilkTea
|
|
|
830
834
|
)
|
|
831
835
|
end
|
|
832
836
|
|
|
837
|
+
# Simulates the actual one-line join: every branch body must be a single
|
|
838
|
+
# physical line with balanced delimiters, and the composed line must fit
|
|
839
|
+
# the column budget. Anything that cannot be joined cleanly is reported
|
|
840
|
+
# as "too wide" so the hint is suppressed.
|
|
833
841
|
def inline_if_too_wide?(statement, stmts)
|
|
834
842
|
return false if statement.branches.empty?
|
|
835
843
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
844
|
+
if_line_idx = Integer(statement.line) - 1
|
|
845
|
+
return true if if_line_idx.negative? || if_line_idx >= @source_lines.length
|
|
846
|
+
|
|
847
|
+
header = @source_lines[if_line_idx].to_s
|
|
848
|
+
return true unless header.strip.start_with?("if ")
|
|
849
|
+
|
|
850
|
+
indent = header[/\A[ \t]*/].to_s
|
|
851
|
+
segments = []
|
|
852
|
+
cur_header = header.strip
|
|
853
|
+
cur_body = nil
|
|
854
|
+
idx = if_line_idx + 1
|
|
855
|
+
while idx < @source_lines.length
|
|
856
|
+
line = @source_lines[idx].to_s.rstrip
|
|
857
|
+
break if line.empty?
|
|
858
|
+
|
|
859
|
+
line_indent = line[/\A[ \t]*/].to_s
|
|
860
|
+
if line_indent == indent && (line.strip == "else:" || line.strip.start_with?("else if "))
|
|
861
|
+
return true if cur_body.nil?
|
|
862
|
+
|
|
863
|
+
segments << [cur_header, cur_body]
|
|
864
|
+
cur_header = line.strip
|
|
865
|
+
cur_body = nil
|
|
866
|
+
idx += 1
|
|
867
|
+
next
|
|
868
|
+
end
|
|
869
|
+
break if line_indent.length <= indent.length
|
|
870
|
+
return true unless cur_body.nil?
|
|
852
871
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return false unless else_src
|
|
856
|
-
text << else_src
|
|
857
|
-
text << " "
|
|
872
|
+
cur_body = line.strip
|
|
873
|
+
idx += 1
|
|
858
874
|
end
|
|
875
|
+
return true if cur_body.nil?
|
|
876
|
+
return true unless cur_header.strip == "else:"
|
|
859
877
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
text << else_body
|
|
878
|
+
segments << [cur_header, cur_body]
|
|
879
|
+
joined = indent + segments.map { |h, b| "#{h} #{b}" }.join(" ")
|
|
880
|
+
joined.length > 120 || !balanced_inline_line?(joined)
|
|
881
|
+
end
|
|
865
882
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
883
|
+
def balanced_inline_line?(text)
|
|
884
|
+
depth = 0
|
|
885
|
+
text.each_char do |ch|
|
|
886
|
+
case ch
|
|
887
|
+
when "(", "["
|
|
888
|
+
depth += 1
|
|
889
|
+
when ")", "]"
|
|
890
|
+
depth -= 1
|
|
891
|
+
return false if depth.negative?
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
depth.zero?
|
|
870
895
|
end
|
|
871
896
|
|
|
872
897
|
def source_line_from(line, column)
|
|
@@ -966,7 +991,9 @@ module MilkTea
|
|
|
966
991
|
struct_type = resolve_expr_type(call)
|
|
967
992
|
return unless struct_type.is_a?(Types::Struct)
|
|
968
993
|
source_type = resolve_expr_type(copied.first.value.receiver)
|
|
969
|
-
|
|
994
|
+
# Compare qualified names: bare names collide across modules
|
|
995
|
+
# (e.g. net::Config vs chan::Config).
|
|
996
|
+
return unless source_type.respond_to?(:to_s) && source_type.to_s == struct_type.to_s
|
|
970
997
|
|
|
971
998
|
field_names = struct_type.fields.keys.map(&:to_s).to_set
|
|
972
999
|
copied_names = copied.map { |arg| arg.name.to_s }.to_set
|
|
@@ -77,6 +77,7 @@ module MilkTea
|
|
|
77
77
|
redundant-else
|
|
78
78
|
redundant-return
|
|
79
79
|
redundant-type-annotation
|
|
80
|
+
redundant-unsafe
|
|
80
81
|
reserved-primitive-name
|
|
81
82
|
trailing-list-comma
|
|
82
83
|
].freeze
|
|
@@ -96,6 +97,7 @@ module MilkTea
|
|
|
96
97
|
"redundant-else" => "Remove redundant else",
|
|
97
98
|
"redundant-return" => "Remove redundant return",
|
|
98
99
|
"redundant-type-annotation" => "Remove redundant type annotation",
|
|
100
|
+
"redundant-unsafe" => "Remove redundant unsafe",
|
|
99
101
|
"prefer-let-else" => "Rewrite as let-else",
|
|
100
102
|
"prefer-var-else" => "Rewrite as var-else",
|
|
101
103
|
"prefer-inline-methods" => "Inline methods into struct",
|
|
@@ -358,7 +360,7 @@ module MilkTea
|
|
|
358
360
|
# redundant-else,
|
|
359
361
|
# redundant-return, reserved-primitive-name,
|
|
360
362
|
# trailing-list-comma.
|
|
361
|
-
# Returns the fixed source (may be identical
|
|
363
|
+
# Returns the fixed source (may be identical to nothing-was-fixable input).
|
|
362
364
|
def self.fix_source(source, path: nil, sema_facts: nil, select: nil, ignore: nil, max_passes: 5, profile: nil)
|
|
363
365
|
pass_limit = [max_passes.to_i, 1].max
|
|
364
366
|
current_source = source
|
|
@@ -446,6 +448,11 @@ module MilkTea
|
|
|
446
448
|
working_context = sema_facts ? nil : best_effort_lint_context(source, path:)
|
|
447
449
|
working_sema_facts = sema_facts || working_context[:facts]
|
|
448
450
|
unresolved_import_paths = sema_facts ? Set.new : working_context[:unresolved_import_paths]
|
|
451
|
+
# With unresolved imports the analysis facts are incomplete (receiver
|
|
452
|
+
# editability, casts, ownership), so hint rules can misfire and fixes
|
|
453
|
+
# would land on false positives. Leave such sources to the author.
|
|
454
|
+
return source if unresolved_import_paths.any?
|
|
455
|
+
|
|
449
456
|
warnings = lint_source(
|
|
450
457
|
source,
|
|
451
458
|
path:,
|