Class: Moult::Index

Inherits:
Object
  • Object
show all
Defined in:
lib/moult/index.rb

Overview

The definition/reference index — Moult's adapter over the rubydex gem and the only file that names Rubydex. Everything downstream consumes the Moult-owned Definition value object, never a rubydex type, so the backend is swappable (the "swap, not rewrite" invariant).

rubydex has two quirks this adapter normalises away (see test/test_index.rb):

  • Its locations are 0-based; Moult/Prism are 1-based. We add 1 to line numbers so dead-code symbol ids line up with Scoring's hotspot ids.
  • Method references are not resolved to their target declaration (only constants are). So a method is considered "referenced" when its bare name appears anywhere in the call-site collection. This is deliberately conservative: a name collision can only hide a dead method, never invent a false positive — the safe direction for a confidence-graded tool.

Defined Under Namespace

Classes: Definition, Edge

Constant Summary collapse

BUILTIN_SCHEME =
"file:"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(graph:, root:) ⇒ Index

Returns a new instance of Index.



76
77
78
79
80
81
82
83
84
# File 'lib/moult/index.rb', line 76

def initialize(graph:, root:)
  @graph = graph
  # rubydex reports canonical (symlink-resolved) paths, so the root must be
  # canonicalised too or workspace filtering misses everything on systems
  # where e.g. /tmp -> /private/tmp.
  @root = File.realpath(root.to_s)
rescue Errno::ENOENT
  @root = root.to_s
end

Instance Attribute Details

#override_ofObject (readonly)

FQ name of the ancestor whose method this overrides/implements (reachable via that interface), else nil



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

#owner_hierarchy_reference_pathsObject (readonly)

for methods: root-relative paths referencing the owner type or any descendant, excluding the hierarchy's own definition files. nil when the owner is unknown or defined outside the workspace (fact unavailable), and always nil for constants (their own reference_paths already carry the signal)



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

#reference_countObject (readonly)

resolvable usages of this definition



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

#reference_pathsObject (readonly)

root-relative paths that use it



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

Class Method Details

.available?Boolean

Whether the rubydex backend is loadable. Always true once the gem is a hard dependency, but kept so live integration tests can skip cleanly.

Returns:

  • (Boolean)


68
69
70
71
72
73
# File 'lib/moult/index.rb', line 68

def available?
  require "rubydex"
  true
rescue LoadError
  false
end

.build(root:, paths:) ⇒ Index

Parameters:

  • root (String)

    absolute analysis root

  • paths (Array<String>)

    absolute paths of Ruby files to index

Returns:



57
58
59
60
61
62
63
64
# File 'lib/moult/index.rb', line 57

def build(root:, paths:)
  graph = Rubydex::Graph.new
  graph.index_all(Array(paths))
  graph.resolve
  new(graph: graph, root: root)
rescue => e
  raise Moult::Error, "rubydex indexing failed: #{e.class}: #{e.message}"
end

Instance Method Details

#definitionsArray<Index::Definition>

Returns method + constant definition sites defined within the workspace, each with its resolved reference count.

Returns:

  • (Array<Index::Definition>)

    method + constant definition sites defined within the workspace, each with its resolved reference count.



88
89
90
# File 'lib/moult/index.rb', line 88

def definitions
  @definitions ||= method_definitions + constant_definitions
end

#diagnosticsArray<String>

Returns human-readable index diagnostics (non-fatal).

Returns:

  • (Array<String>)

    human-readable index diagnostics (non-fatal).



97
98
99
100
101
# File 'lib/moult/index.rb', line 97

def diagnostics
  @graph.diagnostics.map(&:to_s)
rescue
  []
end

#file_edgesArray<Index::Edge>

Returns unique src->dst file dependencies from resolved constant references (superclass and mixin clauses flow through the same list, so inheritance edges are included). A constant reopened in N files yields an edge to every in-workspace definition file; self-edges and qualifier segments are dropped. Sorted by [src, dst] so output is byte-stable regardless of rubydex iteration order.

Returns:

  • (Array<Index::Edge>)

    unique src->dst file dependencies from resolved constant references (superclass and mixin clauses flow through the same list, so inheritance edges are included). A constant reopened in N files yields an edge to every in-workspace definition file; self-edges and qualifier segments are dropped. Sorted by [src, dst] so output is byte-stable regardless of rubydex iteration order.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/moult/index.rb', line 109

def file_edges
  @file_edges ||= begin
    edges = {}
    resolved = @graph.constant_references.select { |r| r.is_a?(Rubydex::ResolvedConstantReference) }
    qualifiers = qualifier_references(resolved)
    resolved.each do |ref|
      next if qualifiers.include?(ref)
      src = workspace_relative(ref.location)
      next unless src
      span = span_from(ref.location)
      in_workspace_definitions(ref.declaration).each do |_defn, _span, dst|
        next if dst == src
        existing = edges[[src, dst]]
        next if existing && existing.span.start_line <= span.start_line
        edges[[src, dst]] = Edge.new(src: src, dst: dst, constant: ref.declaration.name, span: span)
      end
    end
    edges.values.sort_by { |e| [e.src, e.dst] }
  end
end

#kind=(value) ⇒ Object

:method or :constant



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

#resolved?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/moult/index.rb', line 92

def resolved?
  true
end

#singleton=(value) ⇒ Object

true for Class.method / constants



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)

#visibility=(value) ⇒ Object

:public, :private, :protected



39
40
41
42
43
44
# File 'lib/moult/index.rb', line 39

Definition = Struct.new(
  :symbol_id, :kind, :name, :unqualified_name, :owner,
  :visibility, :singleton, :span, :path,
  :reference_count, :reference_paths, :override_of,
  :owner_hierarchy_reference_paths
)