Skip to content

Commit 2fc33f7

Browse files
committed
Add --show-stats option (for experiment)
1 parent c0af67e commit 2fc33f7

6 files changed

Lines changed: 332 additions & 1 deletion

File tree

lib/typeprof/cli/cli.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def initialize(argv)
3838
opt.on("--[no-]show-errors", "Display possible errors found during the analysis") {|v| core_options[:output_diagnostics] = v }
3939
opt.on("--[no-]show-parameter-names", "Display parameter names for methods") {|v| core_options[:output_parameter_names] = v }
4040
opt.on("--[no-]show-source-locations", "Display definition source locations for methods") {|v| core_options[:output_source_locations] = v }
41+
opt.on("--[no-]show-stats", "Display type inference statistics after analysis (for debugging purpose)") {|v| core_options[:output_stats] = v }
4142

4243
opt.separator ""
4344
opt.separator "Advanced options:"
@@ -67,6 +68,7 @@ def initialize(argv)
6768
output_errors: false,
6869
output_parameter_names: false,
6970
output_source_locations: false,
71+
output_stats: false,
7072
exclude_patterns: exclude_patterns,
7173
}.merge(core_options)
7274

lib/typeprof/core/graph/box.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ def initialize(node, genv, cpath, singleton, mid, f_args, ret_boxes)
418418

419419
attr_accessor :node
420420

421-
attr_reader :cpath, :singleton, :mid, :f_args, :ret
421+
attr_reader :cpath, :singleton, :mid, :f_args, :ret, :record_block
422422

423423
def destroy(genv)
424424
me = genv.resolve_method(@cpath, @singleton, @mid)

lib/typeprof/core/service.rb

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,230 @@ def batch(files, output)
557557
end
558558
output.puts dump_declarations(file)
559559
end
560+
561+
if @options[:output_stats]
562+
rb_files = show_files.reject {|f| File.extname(f) == ".rbs" }
563+
stats = collect_stats(rb_files)
564+
output.puts
565+
output.puts format_stats(stats)
566+
end
567+
end
568+
569+
def collect_stats(files)
570+
file_stats = []
571+
572+
files.each do |path|
573+
methods = []
574+
constants = []
575+
seen_ivars = Set[]
576+
ivars = []
577+
seen_cvars = Set[]
578+
cvars = []
579+
seen_gvars = Set[]
580+
gvars = []
581+
582+
@rb_text_nodes[path]&.traverse do |event, node|
583+
next unless event == :enter
584+
585+
node.boxes(:mdef) do |mdef|
586+
param_slots = []
587+
f = mdef.f_args
588+
[f.req_positionals, f.opt_positionals, f.post_positionals, f.req_keywords, f.opt_keywords].each do |ary|
589+
ary.each {|vtx| param_slots << classify_vertex(vtx) }
590+
end
591+
[f.rest_positionals, f.rest_keywords].each do |vtx|
592+
param_slots << classify_vertex(vtx) if vtx
593+
end
594+
595+
is_initialize = mdef.mid == :initialize
596+
ret_slots = is_initialize ? [] : [classify_vertex(mdef.ret)]
597+
598+
blk = mdef.record_block
599+
block_param_slots = []
600+
block_ret_slots = []
601+
if blk.used
602+
blk.f_args.each {|vtx| block_param_slots << classify_vertex(vtx) }
603+
block_ret_slots << classify_vertex(blk.ret)
604+
end
605+
606+
methods << {
607+
mid: mdef.mid,
608+
singleton: mdef.singleton,
609+
param_slots: param_slots,
610+
ret_slots: ret_slots,
611+
block_param_slots: block_param_slots,
612+
block_ret_slots: block_ret_slots,
613+
}
614+
end
615+
616+
if node.is_a?(AST::ConstantWriteNode) && node.static_cpath
617+
constants << classify_vertex(node.ret)
618+
end
619+
620+
if node.is_a?(AST::InstanceVariableWriteNode)
621+
scope = node.lenv.cref.scope_level
622+
if scope == :class || scope == :instance
623+
key = [node.lenv.cref.cpath, scope == :class, node.var]
624+
unless seen_ivars.include?(key)
625+
seen_ivars << key
626+
ve = @genv.resolve_ivar(key[0], key[1], key[2])
627+
ivars << classify_vertex(ve.vtx)
628+
end
629+
end
630+
end
631+
632+
if node.is_a?(AST::ClassVariableWriteNode)
633+
key = [node.lenv.cref.cpath, node.var]
634+
unless seen_cvars.include?(key)
635+
seen_cvars << key
636+
ve = @genv.resolve_cvar(key[0], key[1])
637+
cvars << classify_vertex(ve.vtx)
638+
end
639+
end
640+
641+
if node.is_a?(AST::GlobalVariableWriteNode)
642+
unless seen_gvars.include?(node.var)
643+
seen_gvars << node.var
644+
ve = @genv.resolve_gvar(node.var)
645+
gvars << classify_vertex(ve.vtx)
646+
end
647+
end
648+
end
649+
650+
file_stats << {
651+
path: path,
652+
methods: methods,
653+
constants: constants,
654+
ivars: ivars,
655+
cvars: cvars,
656+
gvars: gvars,
657+
}
658+
end
659+
660+
file_stats
661+
end
662+
663+
def classify_vertex(vtx)
664+
vtx.types.empty? ? :untyped : :typed
665+
end
666+
667+
def format_stats(stats)
668+
total_methods = 0
669+
fully_typed = 0
670+
partially_typed = 0
671+
fully_untyped = 0
672+
673+
slot_categories = %i[param ret blk_param blk_ret const ivar cvar gvar]
674+
typed = Hash.new(0)
675+
untyped = Hash.new(0)
676+
677+
file_summaries = []
678+
679+
stats.each do |file|
680+
f_typed = 0
681+
f_total = 0
682+
683+
file[:methods].each do |m|
684+
total_methods += 1
685+
686+
method_slot_keys = %i[param_slots ret_slots block_param_slots block_ret_slots]
687+
category_keys = %i[param ret blk_param blk_ret]
688+
689+
all_slots = method_slot_keys.flat_map {|k| m[k] }
690+
691+
method_slot_keys.zip(category_keys) do |slot_key, cat|
692+
m[slot_key].each do |s|
693+
if s == :typed
694+
typed[cat] += 1
695+
else
696+
untyped[cat] += 1
697+
end
698+
end
699+
end
700+
701+
if all_slots.empty? || all_slots.all? {|s| s == :typed }
702+
fully_typed += 1
703+
elsif all_slots.none? {|s| s == :typed }
704+
fully_untyped += 1
705+
else
706+
partially_typed += 1
707+
end
708+
709+
f_typed += all_slots.count(:typed)
710+
f_total += all_slots.size
711+
end
712+
713+
%i[constants ivars cvars gvars].zip(%i[const ivar cvar gvar]) do |data_key, cat|
714+
file[data_key].each do |s|
715+
f_total += 1
716+
if s == :typed
717+
typed[cat] += 1
718+
f_typed += 1
719+
else
720+
untyped[cat] += 1
721+
end
722+
end
723+
end
724+
725+
if f_total > 0
726+
file_summaries << {
727+
path: file[:path],
728+
methods: file[:methods].size,
729+
typed: f_typed,
730+
total: f_total,
731+
}
732+
end
733+
end
734+
735+
overall_typed = slot_categories.sum {|c| typed[c] }
736+
overall_untyped = slot_categories.sum {|c| untyped[c] }
737+
overall_total = overall_typed + overall_untyped
738+
739+
labels = {
740+
param: "Parameter slots",
741+
ret: "Return slots",
742+
blk_param: "Block parameter slots",
743+
blk_ret: "Block return slots",
744+
const: "Constants",
745+
ivar: "Instance variables",
746+
cvar: "Class variables",
747+
gvar: "Global variables",
748+
}
749+
750+
lines = []
751+
lines << "# TypeProf Evaluation Statistics"
752+
lines << "#"
753+
lines << "# Total methods: #{ total_methods }"
754+
lines << "# Fully typed: #{ fully_typed }"
755+
lines << "# Partially typed: #{ partially_typed }"
756+
lines << "# Fully untyped: #{ fully_untyped }"
757+
758+
slot_categories.each do |cat|
759+
total = typed[cat] + untyped[cat]
760+
lines << "#"
761+
lines << "# #{ labels[cat] }: #{ total }"
762+
lines << "# Typed: #{ typed[cat] } (#{ pct(typed[cat], total) })"
763+
lines << "# Untyped: #{ untyped[cat] } (#{ pct(untyped[cat], total) })"
764+
end
765+
766+
lines << "#"
767+
lines << "# Overall: #{ overall_typed }/#{ overall_total } typed (#{ pct(overall_typed, overall_total) })"
768+
lines << "# #{ overall_untyped }/#{ overall_total } untyped (#{ pct(overall_untyped, overall_total) })"
769+
770+
if file_summaries.size > 1
771+
lines << "#"
772+
lines << "# Per-file breakdown:"
773+
file_summaries.each do |fs|
774+
lines << "# #{ fs[:path] }: #{ fs[:methods] } methods, #{ fs[:typed] }/#{ fs[:total] } typed (#{ pct(fs[:typed], fs[:total]) })"
775+
end
776+
end
777+
778+
lines.join("\n")
779+
end
780+
781+
def pct(n, total)
782+
return "0.0%" if total == 0
783+
"#{ (n * 100.0 / total).round(1) }%"
560784
end
561785

562786
private

test/cli_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,58 @@ def foo: (String) -> String
122122
END
123123
end
124124

125+
def test_e2e_show_stats
126+
result = test_run("show_stats", ["--no-show-typeprof-version", "--show-stats", "."])
127+
stats = result[/# TypeProf Evaluation Statistics.*/m]
128+
assert(stats, "--show-stats should output statistics section")
129+
130+
# Method summary: 6 methods total
131+
# initialize (no slots) → fully typed
132+
# typed_method (param typed, ret typed) → fully typed
133+
# with_typed_block (ret typed, block_param typed, block_ret typed) → fully typed
134+
# untyped_params (2 params untyped, ret typed) → partially typed
135+
# with_untyped_block (ret untyped, block_param untyped, block_ret untyped) → fully untyped
136+
# uncalled_writer (param untyped, ret untyped) → fully untyped
137+
assert_include(stats, "# Total methods: 6")
138+
assert_include(stats, "# Fully typed: 3")
139+
assert_include(stats, "# Partially typed: 1")
140+
assert_include(stats, "# Fully untyped: 2")
141+
142+
# Parameter slots: typed_method(1 typed) + untyped_params(2 untyped) + uncalled_writer(1 untyped)
143+
assert_include(stats, "# Parameter slots: 4\n# Typed: 1 (25.0%)\n# Untyped: 3 (75.0%)")
144+
145+
# Return slots: typed_method(typed) + untyped_params(typed nil) + with_typed_block(typed)
146+
# + with_untyped_block(untyped) + uncalled_writer(untyped)
147+
assert_include(stats, "# Return slots: 5\n# Typed: 3 (60.0%)\n# Untyped: 2 (40.0%)")
148+
149+
# Block parameter slots: with_typed_block(1 typed) + with_untyped_block(1 untyped)
150+
assert_include(stats, "# Block parameter slots: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
151+
152+
# Block return slots: with_typed_block(1 typed) + with_untyped_block(1 untyped)
153+
assert_include(stats, "# Block return slots: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
154+
155+
# Constants: TYPED_CONST(typed) + Foo::UNTYPED_CONST(untyped)
156+
assert_include(stats, "# Constants: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
157+
158+
# Instance variables: @typed_ivar(typed) + @untyped_ivar(untyped)
159+
assert_include(stats, "# Instance variables: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
160+
161+
# Class variables: @@typed_cvar(typed) + @@untyped_cvar(untyped)
162+
assert_include(stats, "# Class variables: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
163+
164+
# Global variables: $typed_gvar(typed) + $untyped_gvar(untyped)
165+
assert_include(stats, "# Global variables: 2\n# Typed: 1 (50.0%)\n# Untyped: 1 (50.0%)")
166+
167+
# Overall: 10 typed out of 21
168+
assert_include(stats, "# Overall: 10/21 typed (47.6%)")
169+
assert_include(stats, "# 11/21 untyped (52.4%)")
170+
end
171+
172+
def test_e2e_no_show_stats
173+
result = test_run("basic", ["--no-show-typeprof-version", "."])
174+
assert_not_include(result, "TypeProf Evaluation Statistics")
175+
end
176+
125177
def test_lsp_options_with_lsp_mode
126178
assert_nothing_raised { TypeProf::CLI::CLI.new(["--lsp", "--stdio"]) }
127179
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Typed constant
2+
TYPED_CONST = 1
3+
4+
# Typed global variable
5+
$typed_gvar = 99
6+
7+
class Foo
8+
# Untyped constant: assigned from unwritten class-level ivar
9+
UNTYPED_CONST = @unset
10+
11+
# Typed class variable
12+
@@typed_cvar = "hello"
13+
14+
def initialize
15+
# Typed instance variable
16+
@typed_ivar = 42
17+
end
18+
19+
# Fully typed method: param and return both typed
20+
def typed_method(n)
21+
n
22+
end
23+
24+
# Partially typed method: params untyped (never called), return typed (nil)
25+
def untyped_params(a, b)
26+
end
27+
28+
# Method with typed block: yields a typed value
29+
def with_typed_block
30+
yield 1
31+
end
32+
33+
# Method with untyped block: yields an untyped value (unwritten ivar)
34+
def with_untyped_block
35+
yield @nonexistent
36+
end
37+
38+
# Never called: param 'a' has no type, so ivar/cvar/gvar assigned from it are untyped
39+
def uncalled_writer(a)
40+
@untyped_ivar = a
41+
@@untyped_cvar = a
42+
$untyped_gvar = a
43+
end
44+
end
45+
46+
Foo.new.typed_method("str")
47+
Foo.new.with_typed_block {|x| x.to_s }
48+
Foo.new.with_untyped_block {|x| x }
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"typeprof_version": "experimental",
3+
"rbs_dir": "sig/",
4+
"analysis_unit_dirs": ["."]
5+
}

0 commit comments

Comments
 (0)