Module: Moult::DeadCode
- Defined in:
- lib/moult/dead_code.rb
Overview
Orchestrates the dead-code analysis: it asks the Index for every definition, keeps the ones with no production reference, gathers the facts each finding is judged on, and runs them through the pure Confidence model. The result is a ranked DeadCodeReport of confidence-graded candidates — never assertions of certain death.
This is the only layer that knows how the facts are sourced (the index, the Rails conventions, a metaprogramming scan of the owning file); Confidence stays a pure function of those facts so it can be tested in isolation.
Constant Summary collapse
- TEST_PATH =
%r{(\A|/)(test|spec)/}- DYNAMIC_TOKENS =
Tokens that indicate dynamic dispatch / metaprogramming in a file. Their mere presence lowers confidence for definitions in that file: such code can be reached in ways static analysis cannot see. Matched conservatively (a false match only lowers confidence, never hides a finding).
/ \b( send | public_send | __send__ | method_missing | respond_to_missing\? | define_method | define_singleton_method | class_eval | module_eval | instance_eval | instance_exec | const_get | const_set | constantize | eval )\b /x
Class Method Summary collapse
- .build_report(root:, files:, index:, rails:, min_confidence: 0.0, git_ref: nil, generated_at: nil, backend_version: nil, coverage: nil) ⇒ DeadCodeReport
-
.candidate?(definition) ⇒ Boolean
A definition is a candidate when nothing outside of tests references it.
-
.constant_liveness(index) ⇒ Object
FQ constant name => whether it has a production (non-test) reference.
- .context_for(definition, index:, rails:, dynamic_files:, coverage: nil, constant_live: {}) ⇒ Object
-
.dynamic_dispatch_files(files, root) ⇒ Set<String>
Root-relative paths whose source contains dynamic dispatch.
-
.hierarchy_referenced?(definition) ⇒ Boolean
Whether the owner type or any descendant is referenced outside tests.
- .non_test_reference_paths(definition) ⇒ Object
-
.runtime_for(definition, coverage) ⇒ Object
The runtime classification for this definition, joined on the same path + span that make up its symbol_id.
-
.test_only?(definition) ⇒ Boolean
Referenced only from test/spec files: it is exercised, but possibly only to keep otherwise-dead production code alive — a weaker candidate, not excluded.
Class Method Details
.build_report(root:, files:, index:, rails:, min_confidence: 0.0, git_ref: nil, generated_at: nil, backend_version: nil, coverage: nil) ⇒ DeadCodeReport
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/moult/dead_code.rb', line 40 def build_report(root:, files:, index:, rails:, min_confidence: 0.0, git_ref: nil, generated_at: nil, backend_version: nil, coverage: nil) dynamic_files = dynamic_dispatch_files(files, root) constant_live = constant_liveness(index) findings = index.definitions.filter_map do |definition| next unless candidate?(definition) Confidence.score(context_for(definition, index: index, rails: rails, dynamic_files: dynamic_files, coverage: coverage, constant_live: constant_live)) end findings.select! { |f| f.confidence >= min_confidence } findings.sort_by! { |f| [-f.confidence, f.name.to_s] } DeadCodeReport.new( root: root, findings: findings, git_ref: git_ref, generated_at: generated_at, backend: "rubydex", backend_version: backend_version, resolved: index.resolved?, rails: rails.rails?, diagnostics: index.diagnostics, coverage_source: coverage&.source ) end |
.candidate?(definition) ⇒ Boolean
A definition is a candidate when nothing outside of tests references it.
69 70 71 |
# File 'lib/moult/dead_code.rb', line 69 def candidate?(definition) non_test_reference_paths(definition).empty? end |
.constant_liveness(index) ⇒ Object
FQ constant name => whether it has a production (non-test) reference. The liveness join for override_live: an override's ancestor method shares the override's name-keyed references, so the honest signal is the ancestor type's constant references. Reopened constants OR together; a name absent from this map (Object, gems) stays nil — unknown.
109 110 111 112 113 114 |
# File 'lib/moult/dead_code.rb', line 109 def constant_liveness(index) index.definitions.each_with_object({}) do |d, acc| next unless d.kind == :constant acc[d.name] = acc[d.name] || non_test_reference_paths(d).any? end end |
.context_for(definition, index:, rails:, dynamic_files:, coverage: nil, constant_live: {}) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/moult/dead_code.rb', line 73 def context_for(definition, index:, rails:, dynamic_files:, coverage: nil, constant_live: {}) Confidence::Context.new( symbol_id: definition.symbol_id, kind: definition.kind, name: definition.name, span: definition.span, path: definition.path, visibility: definition.visibility, reference_count: definition.reference_count, test_only: test_only?(definition), rails_signals: rails.signals_for(definition), dynamic_dispatch: dynamic_files.include?(definition.path), override_of: definition.override_of, deprecated: false, index_resolved: index.resolved?, runtime: runtime_for(definition, coverage), override_live: definition.override_of && constant_live[definition.override_of], hierarchy_referenced: hierarchy_referenced?(definition) ) end |
.dynamic_dispatch_files(files, root) ⇒ Set<String>
Returns root-relative paths whose source contains dynamic dispatch.
136 137 138 139 140 141 142 143 |
# File 'lib/moult/dead_code.rb', line 136 def dynamic_dispatch_files(files, root) files.each_with_object(Set.new) do |abs, set| source = File.read(abs) set << SymbolId.relative_path(abs, root) if source.match?(DYNAMIC_TOKENS) rescue next end end |
.hierarchy_referenced?(definition) ⇒ Boolean
Whether the owner type or any descendant is referenced outside tests. nil (unknown) propagates from the index; test filtering stays here, matching the reference_paths convention — the Index never learns what a test path is.
98 99 100 101 102 |
# File 'lib/moult/dead_code.rb', line 98 def hierarchy_referenced?(definition) paths = definition.owner_hierarchy_reference_paths return nil if paths.nil? Array(paths).reject { |path| path.to_s.match?(TEST_PATH) }.any? end |
.non_test_reference_paths(definition) ⇒ Object
131 132 133 |
# File 'lib/moult/dead_code.rb', line 131 def non_test_reference_paths(definition) Array(definition.reference_paths).reject { |path| path.to_s.match?(TEST_PATH) } end |
.runtime_for(definition, coverage) ⇒ Object
The runtime classification for this definition, joined on the same path + span that make up its symbol_id. nil when no coverage was supplied.
118 119 120 121 122 123 |
# File 'lib/moult/dead_code.rb', line 118 def runtime_for(definition, coverage) return nil unless coverage Coverage::Resolver.classify( coverage, path: definition.path, span: definition.span, kind: definition.kind ) end |
.test_only?(definition) ⇒ Boolean
Referenced only from test/spec files: it is exercised, but possibly only to keep otherwise-dead production code alive — a weaker candidate, not excluded.
127 128 129 |
# File 'lib/moult/dead_code.rb', line 127 def test_only?(definition) definition.reference_count.to_i.positive? && non_test_reference_paths(definition).empty? end |