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
# 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)
      # simplecov:disable 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
      # simplecov:enable
    end

    @available = true
  rescue LoadError => e
    # simplecov:disable 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
    # simplecov:enable
  rescue NameError => e
    # simplecov:disable 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
    # simplecov:enable
  rescue TypeError => e
    # simplecov:disable 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')
    warn("CitrusGrammarFinder: TypeError backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}") if debug
    @available = false
    # simplecov:enable
  rescue StandardError => e
    # simplecov:disable defensive - catch-all for unexpected errors
    # Always warn about unexpected errors
    warn("CitrusGrammarFinder: Unexpected error: #{e.class}: #{e.message}")
    warn("CitrusGrammarFinder: backtrace:\n  #{e.backtrace&.first(10)&.join("\n  ")}") if debug
    @available = false
    # simplecov:enable
  end

  @available
end

#grammar_moduleModule?

Get the resolved grammar module

Returns:

  • (Module, nil)

    the grammar module if available



150
151
152
153
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 150

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



195
196
197
198
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 195

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 163

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

    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



181
182
183
184
185
186
187
188
189
190
# File 'lib/tree_haver/citrus_grammar_finder.rb', line 181

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