Class: Mbeditor::RubyDefinitionService

Inherits:
Object
  • Object
show all
Defined in:
app/services/mbeditor/ruby_definition_service.rb

Overview

Searches .rb files in a workspace for definitions of a named Ruby method using Ripper’s AST parser (no subprocesses).

Returns an array of hashes, each describing one definition site:

{
  file:      String,  # workspace-relative path
  line:      Integer, # 1-based line number of the `def` keyword
  signature: String,  # trimmed source text of the def line
  comments:  String   # leading # comment lines immediately above the def (may be empty)
}

Usage:

results = RubyDefinitionService.call(workspace_root, "my_method",
                                     excluded_dirnames: %w[tmp .git])

Constant Summary collapse

MAX_RESULTS =
20
MAX_COMMENT_LOOKAHEAD =
15
MAX_FILES_SCANNED =
10_000

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: []) ⇒ RubyDefinitionService

Returns a new instance of RubyDefinitionService.



95
96
97
98
99
100
# File 'app/services/mbeditor/ruby_definition_service.rb', line 95

def initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
  @workspace_root   = workspace_root.to_s.chomp("/")
  @symbol           = symbol
  @excluded_dirnames = Array(excluded_dirnames)
  @excluded_paths    = Array(excluded_paths)
end

Class Attribute Details

.cache_pathObject

Returns the value of attribute cache_path.



39
40
41
# File 'app/services/mbeditor/ruby_definition_service.rb', line 39

def cache_path
  @cache_path
end

.file_cacheObject (readonly)

Returns the value of attribute file_cache.



38
39
40
# File 'app/services/mbeditor/ruby_definition_service.rb', line 38

def file_cache
  @file_cache
end

.mutexObject (readonly)

Returns the value of attribute mutex.



38
39
40
# File 'app/services/mbeditor/ruby_definition_service.rb', line 38

def mutex
  @mutex
end

Class Method Details

.call(workspace_root, symbol, excluded_dirnames: [], excluded_paths: []) ⇒ Object



41
42
43
44
45
# File 'app/services/mbeditor/ruby_definition_service.rb', line 41

def call(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
  new(workspace_root, symbol,
      excluded_dirnames: excluded_dirnames,
      excluded_paths: excluded_paths).call
end

.clear_cache!Object

Exposed for tests.



86
87
88
89
90
91
92
# File 'app/services/mbeditor/ruby_definition_service.rb', line 86

def clear_cache!
  @mutex.synchronize { @file_cache.clear; @cache_loaded = false }
  path = @cache_path.to_s
  File.delete(path) if !path.empty? && File.exist?(path)
rescue StandardError
  nil
end

.load_disk_cache_onceObject

Load the JSON cache from disk exactly once per process (double-checked under the mutex so concurrent first-calls don’t double-load).



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/services/mbeditor/ruby_definition_service.rb', line 49

def load_disk_cache_once
  return if @cache_loaded

  @mutex.synchronize do
    return if @cache_loaded

    @cache_loaded = true
    path = @cache_path.to_s
    return if path.empty? || !File.exist?(path)

    raw = JSON.parse(File.read(path))
    raw.each do |abs_path, entry|
      @file_cache[abs_path] = {
        mtime:    entry["mtime"].to_f,
        lines:    entry["lines"],
        all_defs: entry["all_defs"]
      }
    end
  rescue StandardError
    nil # corrupted or incompatible cache file — start fresh
  end
end

.persist_cacheObject

Atomically write the in-memory cache to disk (tmp-file + rename).



73
74
75
76
77
78
79
80
81
82
83
# File 'app/services/mbeditor/ruby_definition_service.rb', line 73

def persist_cache
  path = @cache_path.to_s
  return if path.empty?

  snapshot = @mutex.synchronize { @file_cache.dup }
  tmp_path = "#{path}.tmp"
  File.write(tmp_path, JSON.generate(snapshot))
  File.rename(tmp_path, path)
rescue StandardError
  nil
end

Instance Method Details

#callObject



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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'app/services/mbeditor/ruby_definition_service.rb', line 102

def call
  self.class.load_disk_cache_once

  results       = []
  @new_entries  = false
  files_scanned = 0

  evict_deleted_cache_entries

  Find.find(@workspace_root) do |path|
    # Prune excluded directories
    if File.directory?(path)
      dirname = File.basename(path)
      rel_dir = relative_path(path)
      if path != @workspace_root && excluded_dir?(dirname, rel_dir)
        Find.prune
      end
      next
    end

    next unless path.end_with?(".rb")

    rel = relative_path(path)
    next if excluded_rel_path?(rel, File.basename(path))

    files_scanned += 1
    if files_scanned > MAX_FILES_SCANNED
      Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
      break
    end

    begin
      cached = cache_entry_for(path)
      next unless cached

      hit_lines = cached[:all_defs][@symbol]
      next unless hit_lines && hit_lines.any?

      hit_lines.each do |def_line|
        results << {
          file:      rel,
          line:      def_line,
          signature: (cached[:lines][def_line - 1] || "").strip,
          comments:  extract_comments(cached[:lines], def_line)
        }
        return results if results.length >= MAX_RESULTS
      end
    rescue StandardError
      # Malformed file or unreadable; skip silently
    end
  end

  self.class.persist_cache if @new_entries
  results
end