kumi 0.0.9 ā 0.0.11
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/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb ā type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- data/migrate_to_core_iterative.rb +0 -938
@@ -2,11 +2,12 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
# For field metadata declarations inside input blocks
|
6
|
+
InputDeclaration = Struct.new(:name, :domain, :type, :children, :access_mode) do
|
7
|
+
include Node
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
def children = self[:children] || []
|
10
|
+
def access_mode = self[:access_mode]
|
11
|
+
end
|
11
12
|
end
|
12
13
|
end
|
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
# For field usage/reference in expressions (input.field_name)
|
6
|
+
InputElementReference = Struct.new(:path) do
|
7
|
+
include Node
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
def children = []
|
10
|
+
end
|
11
11
|
end
|
12
12
|
end
|
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
# For field usage/reference in expressions (input.field_name)
|
6
|
+
InputReference = Struct.new(:name) do
|
7
|
+
include Node
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
def children = []
|
10
|
+
end
|
11
11
|
end
|
12
12
|
end
|
data/lib/kumi/syntax/literal.rb
CHANGED
data/lib/kumi/syntax/node.rb
CHANGED
@@ -2,45 +2,44 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
Location = Struct.new(:file, :line, :column, keyword_init: true)
|
5
|
+
# A struct to hold standardized source location information.
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Base module included by all AST nodes to provide a standard
|
8
|
+
# interface for accessing source location information..
|
9
|
+
module Node
|
10
|
+
attr_accessor :loc
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
def initialize(*args, loc: nil, **kwargs)
|
13
|
+
@loc = loc
|
14
|
+
super(*args, **kwargs)
|
15
|
+
freeze
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
18
|
+
def ==(other)
|
19
|
+
other.is_a?(self.class) &&
|
20
|
+
# for Struct-based nodes
|
21
|
+
(if respond_to?(:members)
|
22
|
+
members.all? { |m| self[m] == other[m] }
|
23
|
+
else
|
24
|
+
instance_variables.reject { |iv| iv == :@loc }
|
25
|
+
.all? do |iv|
|
26
|
+
instance_variable_get(iv) ==
|
27
|
+
other.instance_variable_get(iv)
|
30
28
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
end
|
30
|
+
)
|
31
|
+
end
|
32
|
+
alias eql? ==
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
34
|
+
def hash
|
35
|
+
values = if respond_to?(:members)
|
36
|
+
members.map { |m| self[m] }
|
37
|
+
else
|
38
|
+
instance_variables.reject { |iv| iv == :@loc }
|
39
|
+
.map { |iv| instance_variable_get(iv) }
|
40
|
+
end
|
41
|
+
[self.class, *values].hash
|
44
42
|
end
|
43
|
+
end
|
45
44
|
end
|
46
45
|
end
|
data/lib/kumi/syntax/root.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
# Represents the root of the Abstract Syntax Tree.
|
6
|
+
# It holds all the top-level declarations parsed from the source.
|
7
|
+
Root = Struct.new(:inputs, :values, :traits) do
|
8
|
+
include Node
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
def children = [inputs, values, traits]
|
11
|
+
end
|
12
12
|
end
|
13
13
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
5
|
+
TraitDeclaration = Struct.new(:name, :expression) do
|
6
|
+
include Node
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
def children = [expression]
|
9
|
+
end
|
10
10
|
end
|
11
11
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Kumi
|
4
4
|
module Syntax
|
5
|
-
|
6
|
-
|
5
|
+
ValueDeclaration = Struct.new(:name, :expression) do
|
6
|
+
include Node
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
def children = [expression]
|
9
|
+
end
|
10
10
|
end
|
11
11
|
end
|
data/lib/kumi/version.rb
CHANGED
data/lib/kumi.rb
CHANGED
@@ -5,22 +5,13 @@ require "zeitwerk"
|
|
5
5
|
|
6
6
|
loader = Zeitwerk::Loader.for_gem
|
7
7
|
loader.ignore("#{__dir__}/kumi-cli")
|
8
|
+
loader.inflector.inflect(
|
9
|
+
"lower_to_ir_pass" => "LowerToIRPass",
|
10
|
+
"vm" => "VM",
|
11
|
+
"ir" => "IR",
|
12
|
+
'ir_dump' => 'IRDump',
|
13
|
+
)
|
8
14
|
loader.setup
|
9
15
|
|
10
16
|
module Kumi
|
11
|
-
extend Schema
|
12
|
-
|
13
|
-
def self.inspector_from_schema
|
14
|
-
Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.reset!
|
18
|
-
@__syntax_tree__ = nil
|
19
|
-
@__analyzer_result__ = nil
|
20
|
-
@__compiled_schema__ = nil
|
21
|
-
@__schema_metadata__ = nil
|
22
|
-
end
|
23
|
-
|
24
|
-
# Reset on require to avoid state leakage in tests
|
25
|
-
reset!
|
26
17
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
coverage_file = "coverage/.resultset.json"
|
7
|
+
|
8
|
+
unless File.exist?(coverage_file)
|
9
|
+
puts "ā No coverage data found. Run tests first:"
|
10
|
+
puts " COVERAGE=true bundle exec rspec"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
data = JSON.parse(File.read(coverage_file))
|
15
|
+
coverage = data.values.first["coverage"]
|
16
|
+
|
17
|
+
broadcast_file = coverage.find { |file, _| file.include?("broadcast_detector.rb") }
|
18
|
+
|
19
|
+
if broadcast_file
|
20
|
+
file_path, line_data = broadcast_file
|
21
|
+
lines = line_data["lines"]
|
22
|
+
|
23
|
+
puts "š BroadcastDetector Coverage Analysis:"
|
24
|
+
puts "File: #{File.basename(file_path)}"
|
25
|
+
|
26
|
+
total_lines = lines.count { |hits| !hits.nil? }
|
27
|
+
covered_lines = lines.count { |hits| hits && hits > 0 }
|
28
|
+
coverage_percent = (covered_lines.to_f / total_lines * 100).round(1)
|
29
|
+
|
30
|
+
puts "Coverage: #{coverage_percent}% (#{covered_lines}/#{total_lines} lines)"
|
31
|
+
|
32
|
+
# Find uncovered method definitions
|
33
|
+
file_content = File.read(file_path)
|
34
|
+
uncovered_methods = []
|
35
|
+
covered_methods = []
|
36
|
+
|
37
|
+
file_content.lines.each_with_index do |line, i|
|
38
|
+
if line.match?(/^\s*def\s+(\w+)/)
|
39
|
+
method_name = line.match(/^\s*def\s+(\w+)/)[1]
|
40
|
+
line_hits = lines[i]
|
41
|
+
|
42
|
+
if line_hits.nil?
|
43
|
+
# Line not tracked
|
44
|
+
elsif line_hits == 0
|
45
|
+
uncovered_methods << "#{method_name} (line #{i+1})"
|
46
|
+
else
|
47
|
+
covered_methods << "#{method_name} (line #{i+1})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
puts "\nš Method Coverage Summary:"
|
53
|
+
puts " Total methods: #{covered_methods.length + uncovered_methods.length}"
|
54
|
+
puts " Covered methods: #{covered_methods.length}"
|
55
|
+
puts " Uncovered methods: #{uncovered_methods.length}"
|
56
|
+
|
57
|
+
if uncovered_methods.any?
|
58
|
+
puts "\nšØ Uncovered methods (potential dead code):"
|
59
|
+
uncovered_methods.each { |m| puts " - #{m}" }
|
60
|
+
end
|
61
|
+
|
62
|
+
if covered_methods.any?
|
63
|
+
puts "\nā
Covered methods:"
|
64
|
+
covered_methods.each { |m| puts " - #{m}" }
|
65
|
+
end
|
66
|
+
else
|
67
|
+
puts "ā BroadcastDetector file not found in coverage data"
|
68
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
coverage_file = "coverage/.resultset.json"
|
7
|
+
target_file = "cascade_executor_builder.rb"
|
8
|
+
|
9
|
+
unless File.exist?(coverage_file)
|
10
|
+
puts "ā No coverage data found. Run tests first"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
data = JSON.parse(File.read(coverage_file))
|
15
|
+
coverage = data.values.first["coverage"]
|
16
|
+
|
17
|
+
file_entry = coverage.find { |file, _| file.include?(target_file) }
|
18
|
+
|
19
|
+
if file_entry
|
20
|
+
file_path, line_data = file_entry
|
21
|
+
lines = line_data["lines"]
|
22
|
+
|
23
|
+
puts "š CascadeExecutorBuilder Method Coverage:"
|
24
|
+
puts "=" * 50
|
25
|
+
|
26
|
+
file_content = File.read(file_path)
|
27
|
+
methods = []
|
28
|
+
|
29
|
+
file_content.lines.each_with_index do |line, i|
|
30
|
+
if line.match?(/^\s*def\s+/)
|
31
|
+
method_match = line.match(/^\s*def\s+(self\.)?(\w+)/)
|
32
|
+
if method_match
|
33
|
+
method_name = method_match[2]
|
34
|
+
is_class_method = !method_match[1].nil?
|
35
|
+
line_hits = lines[i]
|
36
|
+
|
37
|
+
status = if line_hits.nil?
|
38
|
+
"āŖ NOT TRACKED"
|
39
|
+
elsif line_hits == 0
|
40
|
+
"šØ UNCOVERED"
|
41
|
+
else
|
42
|
+
"ā
COVERED (#{line_hits} hits)"
|
43
|
+
end
|
44
|
+
|
45
|
+
prefix = is_class_method ? "self." : ""
|
46
|
+
methods << {
|
47
|
+
name: "#{prefix}#{method_name}",
|
48
|
+
line: i + 1,
|
49
|
+
hits: line_hits,
|
50
|
+
status: status
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
methods.each do |method|
|
57
|
+
printf " %-25s (line %3d) %s\n", method[:name], method[:line], method[:status]
|
58
|
+
end
|
59
|
+
|
60
|
+
uncovered = methods.select { |m| m[:hits] == 0 }
|
61
|
+
covered = methods.select { |m| m[:hits] && m[:hits] > 0 }
|
62
|
+
|
63
|
+
puts "\nš Summary:"
|
64
|
+
puts " Total methods: #{methods.length}"
|
65
|
+
puts " Covered: #{covered.length}"
|
66
|
+
puts " Uncovered: #{uncovered.length}"
|
67
|
+
|
68
|
+
if uncovered.any?
|
69
|
+
puts "\nšØ Uncovered methods (potential dead code):"
|
70
|
+
uncovered.each { |m| puts " #{m[:name]} (line #{m[:line]})" }
|
71
|
+
end
|
72
|
+
else
|
73
|
+
puts "ā #{target_file} not found in coverage data"
|
74
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
coverage_file = "coverage/.resultset.json"
|
7
|
+
|
8
|
+
unless File.exist?(coverage_file)
|
9
|
+
puts "ā No coverage data found. Run tests first"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
data = JSON.parse(File.read(coverage_file))
|
14
|
+
coverage = data.values.first["coverage"]
|
15
|
+
|
16
|
+
broadcasting_files = [
|
17
|
+
"cascade_executor_builder.rb",
|
18
|
+
"vectorized_function_builder.rb",
|
19
|
+
"nested_structure_utils.rb",
|
20
|
+
"broadcast_detector.rb"
|
21
|
+
]
|
22
|
+
|
23
|
+
puts "š Broadcasting Files Coverage:"
|
24
|
+
puts "=" * 60
|
25
|
+
|
26
|
+
broadcasting_files.each do |filename|
|
27
|
+
file_entry = coverage.find { |file, _| file.include?(filename) }
|
28
|
+
|
29
|
+
if file_entry
|
30
|
+
file_path, line_data = file_entry
|
31
|
+
lines = line_data["lines"]
|
32
|
+
|
33
|
+
total_lines = lines.count { |hits| !hits.nil? }
|
34
|
+
covered_lines = lines.count { |hits| hits && hits > 0 }
|
35
|
+
coverage_percent = total_lines > 0 ? (covered_lines.to_f / total_lines * 100).round(1) : 0
|
36
|
+
|
37
|
+
status = if coverage_percent == 0
|
38
|
+
"šØ DEAD"
|
39
|
+
elsif coverage_percent < 20
|
40
|
+
"š“ LOW"
|
41
|
+
elsif coverage_percent < 50
|
42
|
+
"š” MEDIUM"
|
43
|
+
else
|
44
|
+
"ā
GOOD"
|
45
|
+
end
|
46
|
+
|
47
|
+
printf "%-35s %s %6.1f%% (%d/%d lines)\n", filename, status, coverage_percent, covered_lines, total_lines
|
48
|
+
else
|
49
|
+
printf "%-35s ā NOT FOUND\n", filename
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
class SimpleDeadCodeFinder
|
7
|
+
COVERAGE_JSON = "coverage/.resultset.json"
|
8
|
+
|
9
|
+
def run
|
10
|
+
unless File.exist?(COVERAGE_JSON)
|
11
|
+
puts "ā No coverage data found. Run tests first:"
|
12
|
+
puts " COVERAGE=true bundle exec rspec"
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
puts "š Analyzing SimpleCov data for dead code..."
|
17
|
+
|
18
|
+
coverage_data = JSON.parse(File.read(COVERAGE_JSON))
|
19
|
+
coverage = coverage_data.values.first["coverage"]
|
20
|
+
|
21
|
+
find_dead_files(coverage)
|
22
|
+
find_low_coverage_files(coverage)
|
23
|
+
|
24
|
+
puts "\nš” Next steps:"
|
25
|
+
puts " 1. Open coverage/index.html to see detailed line-by-line coverage"
|
26
|
+
puts " 2. Review uncovered files to see if they can be removed"
|
27
|
+
puts " 3. Look at uncovered methods in low-coverage files"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def find_dead_files(coverage)
|
33
|
+
puts "\nšØ COMPLETELY UNCOVERED FILES (potential dead code):"
|
34
|
+
puts "=" * 60
|
35
|
+
|
36
|
+
dead_files = []
|
37
|
+
|
38
|
+
coverage.each do |file, line_data|
|
39
|
+
next unless file.include?("/lib/")
|
40
|
+
|
41
|
+
# Extract lines array from SimpleCov format
|
42
|
+
lines = line_data["lines"]
|
43
|
+
next unless lines
|
44
|
+
|
45
|
+
total_lines = lines.count { |hits| !hits.nil? }
|
46
|
+
covered_lines = lines.count { |hits| hits && hits > 0 }
|
47
|
+
|
48
|
+
if total_lines > 0 && covered_lines == 0
|
49
|
+
relative_path = file.sub("#{Dir.pwd}/", "")
|
50
|
+
dead_files << relative_path
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if dead_files.empty?
|
55
|
+
puts "ā
No completely uncovered files found!"
|
56
|
+
else
|
57
|
+
puts "Found #{dead_files.length} completely uncovered files:"
|
58
|
+
dead_files.sort.each { |f| puts " š #{f}" }
|
59
|
+
|
60
|
+
puts "\nā ļø These files are candidates for removal, but verify:"
|
61
|
+
puts " ⢠Check if they're loaded via autoloading/zeitwerk"
|
62
|
+
puts " ⢠Look for usage in examples/ or external code"
|
63
|
+
puts " ⢠Consider if they're part of public API"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_low_coverage_files(coverage)
|
68
|
+
puts "\nš LOW COVERAGE FILES (<20% - check for dead methods):"
|
69
|
+
puts "=" * 20
|
70
|
+
|
71
|
+
low_coverage = []
|
72
|
+
|
73
|
+
coverage.each do |file, line_data|
|
74
|
+
next unless file.include?("/lib/")
|
75
|
+
|
76
|
+
# Extract lines array from SimpleCov format
|
77
|
+
lines = line_data["lines"]
|
78
|
+
next unless lines
|
79
|
+
|
80
|
+
total_lines = lines.count { |hits| !hits.nil? }
|
81
|
+
covered_lines = lines.count { |hits| hits && hits > 0 }
|
82
|
+
|
83
|
+
next if total_lines == 0
|
84
|
+
|
85
|
+
coverage_percent = (covered_lines.to_f / total_lines * 100)
|
86
|
+
|
87
|
+
next unless coverage_percent > 0 && coverage_percent < 20
|
88
|
+
|
89
|
+
relative_path = file.sub("#{Dir.pwd}/", "")
|
90
|
+
low_coverage << {
|
91
|
+
file: relative_path,
|
92
|
+
percent: coverage_percent,
|
93
|
+
covered: covered_lines,
|
94
|
+
total: total_lines
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
if low_coverage.empty?
|
99
|
+
puts "ā
No files with very low coverage found!"
|
100
|
+
else
|
101
|
+
low_coverage.sort_by { |f| f[:percent] }.each do |info|
|
102
|
+
printf " š %-50s %5.1f%% (%d/%d lines)\n",
|
103
|
+
info[:file], info[:percent], info[:covered], info[:total]
|
104
|
+
end
|
105
|
+
|
106
|
+
puts "\nš” These files likely contain dead methods:"
|
107
|
+
puts " ⢠Open coverage/index.html and click on these files"
|
108
|
+
puts " ⢠Red lines are uncovered - potential dead code"
|
109
|
+
puts " ⢠Look for entire uncovered methods/classes"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
SimpleDeadCodeFinder.new.run if __FILE__ == $0
|