Class: YardUtils

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/yardmcp.rb

Overview

Utility class for YARD operations

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeYardUtils



16
17
18
19
20
21
22
23
# File 'lib/yardmcp.rb', line 16

def initialize
  @libraries = {}
  @object_to_gem = {}
  @last_loaded_gem = nil
  @logger = Logger.new($stderr)
  @logger.level = Logger::INFO unless ENV['DEBUG']
  build_index
end

Instance Attribute Details

#librariesObject (readonly)

Returns the value of attribute libraries.



14
15
16
# File 'lib/yardmcp.rb', line 14

def libraries
  @libraries
end

#loggerObject (readonly)

Returns the value of attribute logger.



14
15
16
# File 'lib/yardmcp.rb', line 14

def logger
  @logger
end

#object_to_gemObject (readonly)

Returns the value of attribute object_to_gem.



14
15
16
# File 'lib/yardmcp.rb', line 14

def object_to_gem
  @object_to_gem
end

Instance Method Details

#ancestors(path) ⇒ Array<String>

Returns the full ancestor chain (superclasses and included modules) for a class or module.



171
172
173
174
175
176
177
178
179
# File 'lib/yardmcp.rb', line 171

def ancestors(path)
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  obj.respond_to?(:inheritance_tree) ? obj.inheritance_tree(true).map(&:path) : []
end

#children(path) ⇒ Array<String>

Lists the children (constants, classes, modules, methods, etc.) under a namespace.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



123
124
125
126
127
128
129
130
131
# File 'lib/yardmcp.rb', line 123

def children(path)
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  obj.respond_to?(:children) ? obj.children.map(&:path) : []
end

#code_snippet(path) ⇒ String?

Fetches the code snippet for a YARD object from installed gems.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



255
256
257
258
259
260
261
262
263
# File 'lib/yardmcp.rb', line 255

def code_snippet(path)
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  obj.respond_to?(:source) ? obj.source : nil
end

#ensure_yardoc_loaded_for_object!(object_path) ⇒ Object

Ensures the correct .yardoc is loaded for the given object path



44
45
46
47
48
49
50
# File 'lib/yardmcp.rb', line 44

def ensure_yardoc_loaded_for_object!(object_path)
  # TODO: Handle multiple gems for the same object path, use some heuristic to determine the correct gem
  gem_name = @object_to_gem[object_path].first
  raise "No documentation found for #{object_path}" unless gem_name

  load_yardoc_for_gem(gem_name)
end

#get_doc(path, gem_name = nil) ⇒ Hash

Fetches documentation and metadata for a YARD object (class/module/method).

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/yardmcp.rb', line 72

def get_doc(path, gem_name = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
  if gem_name
    # Load the specific gem's yardoc
    load_yardoc_for_gem(gem_name)
  else
    ensure_yardoc_loaded_for_object!(path)
  end
  obj = YARD::Registry.at(path)
  raise 'Object not found' unless obj

  tags = obj.tags.map do |tag|
    {
      tag_name: tag.tag_name,
      name: tag.respond_to?(:name) ? tag.name : nil,
      types: tag.respond_to?(:types) ? tag.types : nil,
      text: tag.text
    }
  end

  doc = {
    type: obj.type.to_s,
    name: obj.name.to_s,
    namespace: obj.namespace&.path,
    visibility: obj.respond_to?(:visibility) ? obj.visibility.to_s : nil,
    docstring: obj.docstring.to_s,
    parameters: obj.respond_to?(:parameters) ? obj.parameters : nil,
    return: if obj.respond_to?(:tag) && obj.tag('return')
              {
                types: obj.tag('return').types,
                text: obj.tag('return').text
              }
            end,
    source: obj.respond_to?(:source) ? obj.source : nil,
    tags:
  }

  # Add subclass-specific info
  doc[:attributes] = obj.attributes if obj.respond_to?(:attributes) && obj.attributes
  doc[:constants] = obj.constants.map(&:path) if obj.respond_to?(:constants) && obj.constants
  doc[:superclass] = obj.superclass&.path if obj.respond_to?(:superclass) && obj.superclass
  doc[:scope] = obj.scope if obj.respond_to?(:scope) && obj.scope
  doc[:overridden_method] = obj.overridden_method&.path if obj.respond_to?(:overridden_method) && obj.overridden_method

  doc
end

#hierarchy(path) ⇒ Hash

Returns inheritance and inclusion information for a class or module.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/yardmcp.rb', line 153

def hierarchy(path) # rubocop:disable Metrics/CyclomaticComplexity
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  {
    superclass: obj.respond_to?(:superclass) && obj.superclass ? obj.superclass.path : nil,
    included_modules: obj.respond_to?(:included_modules) ? obj.included_modules.map(&:path) : [],
    mixins: obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : []
  }
end

#list_classes(gem_name) ⇒ Array<String>

Lists all classes and modules in the loaded YARD registry.



62
63
64
65
# File 'lib/yardmcp.rb', line 62

def list_classes(gem_name)
  load_yardoc_for_gem(gem_name)
  YARD::Registry.all(:class, :module).map(&:path).sort
end

#list_gemsArray<String>

Lists all installed gems that have a .yardoc file available.



55
56
57
# File 'lib/yardmcp.rb', line 55

def list_gems
  libraries.keys.sort
end

#load_yardoc_for_gem(gem_name) ⇒ Boolean

Loads the .yardoc file for a given gem into the YARD registry. Caches the last loaded gem to avoid unnecessary reloads.



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/yardmcp.rb', line 30

def load_yardoc_for_gem(gem_name)
  return if @last_loaded_gem == gem_name

  spec = libraries[gem_name].first
  ver = "= #{spec.version}"
  dir = YARD::Registry.yardoc_file_for_gem(spec.name, ver)
  build_docs(gem_name) unless yardoc_exists?(dir)
  raise "Yardoc not found for #{gem_name}" unless yardoc_exists?(dir)

  YARD::Registry.load_yardoc(dir)
  @last_loaded_gem = gem_name
end

#methods_list(path) ⇒ Array<String>

Lists all methods for a class or module.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



138
139
140
141
142
143
144
145
146
# File 'lib/yardmcp.rb', line 138

def methods_list(path)
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  obj.respond_to?(:meths) ? obj.meths.map(&:path) : []
end

Returns related objects: included modules, mixins, and subclasses.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/yardmcp.rb', line 185

def related_objects(path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return {}
  end
  subclasses = YARD::Registry.all(:class).select { |c| c.superclass && c.superclass.path == obj.path }.map(&:path)
  {
    included_modules: obj.respond_to?(:included_modules) ? obj.included_modules.map(&:path) : [],
    mixins: obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : [],
    subclasses:
  }
end

#search(query) ⇒ Array<Hash>

Performs a fuzzy/full-text search in the YARD registry for objects whose path or docstring matches the query.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/yardmcp.rb', line 204

def search(query) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
  require 'levenshtein' unless defined?(Levenshtein)
  results = []
  YARD::Registry.all.each do |obj|
    path = obj.path.to_s
    doc = obj.docstring.to_s
    next if path.empty?

    score = nil
    if path == query
      score = 100
    elsif path.start_with?(query)
      score = 90
    elsif path.include?(query)
      score = 80
    elsif doc.include?(query)
      score = 60
    else
      # Fuzzy match: allow up to 2 edits for short queries, 3 for longer
      dist = Levenshtein.distance(path.downcase, query.downcase)
      score = 70 - dist if dist <= [2, query.length / 3].max
    end
    results << { path:, score: } if score
  end
  # Sort by score descending, then alphabetically
  results.sort_by { |r| [-r[:score], r[:path]] }
end

#source_location(path) ⇒ Hash

Returns the source file and line number for a YARD object (class/module/method).

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/yardmcp.rb', line 237

def source_location(path)
  ensure_yardoc_loaded_for_object!(path)
  obj = YARD::Registry.at(path)
  unless obj
    logger.error "Object not found: #{path}"
    return []
  end
  {
    file: obj.respond_to?(:file) ? obj.file : nil,
    line: obj.respond_to?(:line) ? obj.line : nil
  }
end