Class: TreeHaver::CitrusGrammarFinder

Inherits:
Object
  • Object
show all
Defined in:
lib/tree_haver/citrus_grammar_finder.rb

Overview

Utility for finding and registering Citrus grammar gems.

CitrusGrammarFinder provides language-agnostic discovery of Citrus grammar
gems. Given a language name and gem information, it attempts to load the
grammar and register it with tree_haver.

Unlike tree-sitter grammars (which are .so files), Citrus grammars are
Ruby modules that respond to .parse(source). This class handles the
discovery and registration of these grammars.

Examples:

Basic usage with toml-rb

finder = TreeHaver::CitrusGrammarFinder.new(
  language: :toml,
  gem_name: "toml-rb",
  grammar_const: "TomlRB::Document"
)
finder.register! if finder.available?

With custom require path

finder = TreeHaver::CitrusGrammarFinder.new(
  language: :json,
  gem_name: "json-rb",
  grammar_const: "JsonRB::Grammar",
  require_path: "json/rb"
)

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(language:, gem_name:, grammar_const:, require_path: nil) ⇒ CitrusGrammarFinder

Initialize a Citrus grammar finder

Parameters:

  • language (Symbol, String)

    the language name (e.g., :toml, :json)

  • gem_name (String)

    the gem name (e.g., “toml-rb”)

  • grammar_const (String)

    constant path to grammar (e.g., “TomlRB::Document”)

  • require_path (String, nil) (defaults to: nil)

    custom require path (defaults to gem_name as-is)



50
51
52
53
54
55
56
57
58
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 50

def initialize(language:, gem_name:, grammar_const:, require_path: nil)
  @language_name = language.to_sym
  @gem_name = gem_name
  @grammar_const = grammar_const
  @require_path = require_path || gem_name
  @load_attempted = false
  @available = false
  @grammar_module = nil
end

Instance Attribute Details

#gem_nameString (readonly)

Returns the gem name to require.

Returns:

  • (String)

    the gem name to require



36
37
38
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 36

def gem_name
  @gem_name
end

#grammar_constString (readonly)

Returns the constant path to the grammar (e.g., “TomlRB::Document”).

Returns:

  • (String)

    the constant path to the grammar (e.g., “TomlRB::Document”)



39
40
41
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 39

def grammar_const
  @grammar_const
end

#language_nameSymbol (readonly)

Returns the language identifier.

Returns:

  • (Symbol)

    the language identifier



33
34
35
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 33

def language_name
  @language_name
end

#require_pathString? (readonly)

Returns custom require path (defaults to gem_name with dashes to slashes).

Returns:

  • (String, nil)

    custom require path (defaults to gem_name with dashes to slashes)



42
43
44
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 42

def require_path
  @require_path
end

Instance Method Details

#available?Boolean

Check if the Citrus grammar is available

Attempts to require the gem and resolve the grammar constant.
Result is cached after first call.

Returns:

  • (Boolean)

    true if grammar is available



66
67
68
69
70
71
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
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
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 66

def available?
  return @available if @load_attempted

  @load_attempted = true
  debug = ENV["TREE_HAVER_DEBUG"]

  # Guard against nil require_path (can happen if gem_name was nil)
  if @require_path.nil? || @require_path.empty?
    warn("CitrusGrammarFinder: require_path is nil or empty for #{@language_name}") if debug
    @available = false
    return false
  end

  begin
    # Try to require the gem
    require @require_path

    # Try to resolve the constant
    @grammar_module = resolve_constant(@grammar_const)

    # Verify it responds to parse
    unless @grammar_module.respond_to?(:parse)
      # :nocov: defensive - requires a gem with malformed grammar module
      # Show what methods ARE available to help diagnose the issue
      if debug
        available_methods = @grammar_module.methods(false).sort.first(20)
        warn("CitrusGrammarFinder: #{@grammar_const} doesn't respond to :parse")
        warn("CitrusGrammarFinder: #{@grammar_const}.class = #{@grammar_module.class}")
        warn("CitrusGrammarFinder: #{@grammar_const} is a #{@grammar_module.is_a?(Module) ? "Module" : "non-Module"}")
        warn("CitrusGrammarFinder: Available singleton methods (first 20): #{available_methods.inspect}")
        if @grammar_module.respond_to?(:instance_methods)
          instance_methods = @grammar_module.instance_methods(false).sort.first(20)
          warn("CitrusGrammarFinder: Available instance methods (first 20): #{instance_methods.inspect}")
        end
      end
      @available = false
      return false
      # :nocov:
    end

    @available = true
  rescue LoadError => e
    # :nocov: defensive - requires gem to not be installed
    # Only show LoadError details when debugging
    if debug
      warn("CitrusGrammarFinder: Failed to load '#{@require_path}': #{e.class}: #{e.message}")
      warn("CitrusGrammarFinder: LoadError backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}")
    end
    @available = false
    # :nocov:
  rescue NameError => e
    # :nocov: defensive - requires gem with missing constant
    # Only show NameError details when debugging
    if debug
      warn("CitrusGrammarFinder: Failed to resolve '#{@grammar_const}': #{e.class}: #{e.message}")
      warn("CitrusGrammarFinder: NameError backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}")
    end
    @available = false
    # :nocov:
  rescue TypeError => e
    # :nocov: defensive - TruffleRuby-specific edge case
    # TruffleRuby's bundled_gems.rb can raise TypeError when File.path is called on nil
    # This happens in bundled_gems.rb:124 warning? method when caller locations return nil
    # Always warn about TypeError as it indicates a platform-specific issue
    warn("CitrusGrammarFinder: TypeError during load of '#{@require_path}': #{e.class}: #{e.message}")
    warn("CitrusGrammarFinder: This may be a TruffleRuby bundled_gems.rb issue")
    if debug
      warn("CitrusGrammarFinder: TypeError backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}")
    end
    @available = false
    # :nocov:
  rescue => e
    # :nocov: defensive - catch-all for unexpected errors
    # Always warn about unexpected errors
    warn("CitrusGrammarFinder: Unexpected error: #{e.class}: #{e.message}")
    if debug
      warn("CitrusGrammarFinder: backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}")
    end
    @available = false
    # :nocov:
  end

  @available
end

#grammar_moduleModule?

Get the resolved grammar module

Returns:

  • (Module, nil)

    the grammar module if available



154
155
156
157
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 154

def grammar_module
  available? # Ensure we've tried to load
  @grammar_module
end

#not_found_messageString

Get a human-readable error message when grammar is not found

Returns:

  • (String)

    error message with installation hints



200
201
202
203
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 200

def not_found_message
  "Citrus grammar for #{@language_name} not found. " \
    "Install #{@gem_name} gem: gem install #{@gem_name}"
end

#register!(raise_on_missing: false) ⇒ Boolean

Register this Citrus grammar with TreeHaver

After registration, the language can be used via:
TreeHaver::Language.#language_name

Parameters:

  • raise_on_missing (Boolean) (defaults to: false)

    if true, raises when grammar not available

Returns:

  • (Boolean)

    true if registration succeeded

Raises:

  • (NotAvailable)

    if grammar not available and raise_on_missing is true



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 167

def register!(raise_on_missing: false)
  unless available?
    if raise_on_missing
      raise NotAvailable, not_found_message
    end
    return false
  end

  TreeHaver.register_language(
    @language_name,
    grammar_module: @grammar_module,
    gem_name: @gem_name,
  )
  true
end

#search_infoHash

Get debug information about the search

Returns:

  • (Hash)

    diagnostic information



186
187
188
189
190
191
192
193
194
195
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 186

def search_info
  {
    language: @language_name,
    gem_name: @gem_name,
    grammar_const: @grammar_const,
    require_path: @require_path,
    available: available?,
    grammar_module: @grammar_module&.name,
  }
end