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

Returns a new instance of YardUtils.



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

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.



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

def libraries
  @libraries
end

#loggerObject (readonly)

Returns the value of attribute logger.



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

def logger
  @logger
end

#object_to_gemObject (readonly)

Returns the value of attribute object_to_gem.



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

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.

Parameters:

  • path (String)

    The YARD path of the class/module.

Returns:

  • (Array<String>)

    An array of ancestor paths.



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

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.

Parameters:

  • path (String)

    The YARD path of the namespace.

Returns:

  • (Array<String>)

    An array of child object paths.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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

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.

Parameters:

  • path (String)

    The YARD path (e.g., ‘String#upcase’).

Returns:

  • (String, nil)

    The code snippet if available, otherwise nil.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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

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



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

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).

Parameters:

  • path (String)

    The YARD path (e.g., ‘String#upcase’).

Returns:

  • (Hash)

    A hash containing type, name, namespace, visibility, docstring, parameters, return, and source.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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
117
# File 'lib/yardmcp.rb', line 73

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.

Parameters:

  • path (String)

    The YARD path of the class/module.

Returns:

  • (Hash)

    A hash with :superclass (String or nil), :included_modules (Array<String>), and :mixins (Array<String>).

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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

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.

Returns:

  • (Array<String>)

    An array of fully qualified class/module paths.



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

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.

Returns:

  • (Array<String>)

    An array of gem names with .yardoc files.



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

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.

Parameters:

  • gem_name (String)

    The name of the gem to load.

Returns:

  • (Boolean)

    True if the .yardoc file was loaded, false otherwise.



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

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.

Parameters:

  • path (String)

    The YARD path of the class/module.

Returns:

  • (Array<String>)

    An array of method paths.

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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

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.

Parameters:

  • path (String)

    The YARD path of the class/module.

Returns:

  • (Hash)

    A hash with :included_modules, :mixins, :subclasses.



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

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.

Parameters:

  • query (String)

    The search query string.

Returns:

  • (Array<Hash>)

    An array of hashes with :path and :score for matching object paths, ranked by relevance.



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
231
# File 'lib/yardmcp.rb', line 205

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).

Parameters:

  • path (String)

    The YARD path (e.g., ‘String#upcase’).

Returns:

  • (Hash)

    A hash with :file (String or nil) and :line (Integer or nil).

Raises:

  • (RuntimeError)

    if the object is not found in the registry.



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

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