Class: YardUtils

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

Overview

Utility class for YARD operations

Defined Under Namespace

Classes: AmbiguousObjectError, DocumentationError

Constant Summary collapse

MAX_SOURCE_CHARS =
20_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeYardUtils

Returns a new instance of YardUtils.



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

def initialize
  @libraries = {}
  @object_to_gem = {}
  @last_loaded_gem = nil
  @class_cache = {}
  @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.



30
31
32
# File 'lib/yardmcp.rb', line 30

def libraries
  @libraries
end

#loggerObject (readonly)

Returns the value of attribute logger.



30
31
32
# File 'lib/yardmcp.rb', line 30

def logger
  @logger
end

#object_to_gemObject (readonly)

Returns the value of attribute object_to_gem.



30
31
32
# File 'lib/yardmcp.rb', line 30

def object_to_gem
  @object_to_gem
end

Instance Method Details

#ancestors(path, gem_name = nil) ⇒ 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.



179
180
181
182
# File 'lib/yardmcp.rb', line 179

def ancestors(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  obj.respond_to?(:inheritance_tree) ? obj.inheritance_tree(true).map(&:path) : []
end

#build_docs(gem_name) ⇒ Object



234
235
236
237
238
239
240
241
242
243
# File 'lib/yardmcp.rb', line 234

def build_docs(gem_name)
  gem_spec!(gem_name)
  logger.info "Building docs for #{gem_name}..."
  YARD::CLI::Gems.new.run(gem_name)
  @class_cache.delete(gem_name)
  @last_loaded_gem = nil
  load_yardoc_for_gem(gem_name)
  merge_gem_results([collect_current_gem_objects(gem_name)])
  true
end

#children(path, gem_name = nil) ⇒ 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.



146
147
148
149
# File 'lib/yardmcp.rb', line 146

def children(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  obj.respond_to?(:children) ? obj.children.map(&:path) : []
end

#code_snippet(path, gem_name = nil, max_chars: MAX_SOURCE_CHARS) ⇒ 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.



229
230
231
232
# File 'lib/yardmcp.rb', line 229

def code_snippet(path, gem_name = nil, max_chars: MAX_SOURCE_CHARS)
  obj = object_for!(path, gem_name)
  capped_source(obj.respond_to?(:source) ? obj.source : nil, max_chars:)
end

#ensure_yardoc_loaded_for_object!(object_path) ⇒ Object

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

Raises:



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

def ensure_yardoc_loaded_for_object!(object_path)
  gem_names = @object_to_gem[object_path]
  raise DocumentationError, "No indexed documentation contains '#{object_path}'. Pass gem_name if you know the gem." if gem_names.nil? || gem_names.empty?
  raise AmbiguousObjectError.new(object_path, gem_candidates(gem_names)) if gem_names.uniq.size > 1

  load_yardoc_for_gem(gem_names.first)
end

#gem_candidates(gem_names) ⇒ Object



80
81
82
83
84
85
86
87
# File 'lib/yardmcp.rb', line 80

def gem_candidates(gem_names)
  gem_names.uniq.sort.map do |gem_name|
    {
      gem_name:,
      versions: Array(libraries[gem_name]).map { |library| library.version.to_s }.uniq.sort
    }
  end
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.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/yardmcp.rb', line 102

def get_doc(path, gem_name = nil) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
  obj = object_for!(path, gem_name)

  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: capped_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, gem_name = nil) ⇒ 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.



166
167
168
169
170
171
172
173
# File 'lib/yardmcp.rb', line 166

def hierarchy(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  {
    superclass: obj.respond_to?(:superclass) && obj.superclass ? obj.superclass.path : nil,
    included_modules: obj.respond_to?(:mixins) ? obj.mixins.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.



92
93
94
95
# File 'lib/yardmcp.rb', line 92

def list_classes(gem_name)
  load_yardoc_for_gem(gem_name)
  @class_cache[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.



70
71
72
73
74
# File 'lib/yardmcp.rb', line 70

def list_gems
  libraries.keys.select do |name|
    yardoc_exists?(yardoc_path_for(gem_spec!(name)))
  end.sort
end

#list_installed_gemsObject



76
77
78
# File 'lib/yardmcp.rb', line 76

def list_installed_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.

Raises:



47
48
49
50
51
52
53
54
55
56
# File 'lib/yardmcp.rb', line 47

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

  spec = gem_spec!(gem_name)
  dir = yardoc_path_for(spec)
  raise DocumentationError, "YARD documentation is not indexed for gem '#{gem_name}'" unless yardoc_exists?(dir)

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

#methods_list(path, gem_name = nil) ⇒ 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.



156
157
158
159
# File 'lib/yardmcp.rb', line 156

def methods_list(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  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.



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

def related_objects(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  subclasses = YARD::Registry.all(:class).select { |c| c.superclass && c.superclass.path == obj.path }.map(&:path)
  mixins_list = obj.respond_to?(:mixins) ? obj.mixins.map(&:path) : []
  {
    included_modules: mixins_list,
    mixins: mixins_list,
    subclasses:
  }
end

#search(query, gem_name = nil, limit: 25, offset: 0) ⇒ 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.



203
204
205
206
207
208
209
# File 'lib/yardmcp.rb', line 203

def search(query, gem_name = nil, limit: 25, offset: 0)
  require 'levenshtein' unless defined?(Levenshtein)
  candidates = gem_name ? loaded_objects_for_search(gem_name) : indexed_paths_for_search
  results = candidates.filter_map { |candidate| score_search_candidate(candidate, query) }
  # Sort by score descending, then alphabetically
  results.sort_by { |r| [-r[:score], r[:path]] }.slice(offset, limit) || []
end

#source_location(path, gem_name = nil) ⇒ 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.



216
217
218
219
220
221
222
# File 'lib/yardmcp.rb', line 216

def source_location(path, gem_name = nil)
  obj = object_for!(path, gem_name)
  {
    file: obj.respond_to?(:file) ? obj.file : nil,
    line: obj.respond_to?(:line) ? obj.line : nil
  }
end