Language Implementation Guide: Internal Patterns & Best Practices

October 31, 2025 · View on GitHub

This guide documents the internal implementation patterns extracted from TypeScript and Rust parsers. Follow these patterns for consistency, easier debugging, and maintainability.

Table of Contents

  1. File Structure & Organization
  2. Parser Implementation (parser.rs)
  3. Behavior Implementation (behavior.rs)
  4. Resolution Implementation (resolution.rs)
  5. Definition & Registration (definition.rs)
  6. Module Exports (mod.rs)
  7. Audit System (audit.rs)
  8. Naming Conventions Reference
  9. Common Patterns & Heuristics

File Structure & Organization

src/parsing/{language}/
├── mod.rs         # Public API exports
├── definition.rs  # Registry integration (25-55 lines)
├── parser.rs      # Symbol extraction (1000-3200 lines)
├── behavior.rs    # Language behaviors (600-1200 lines)
├── resolution.rs  # Scoping & resolution (600-1200 lines)
└── audit.rs       # ABI-15 coverage (200-400 lines)

Key Principle: Each file has a single, focused responsibility.


Parser Implementation (parser.rs)

Struct Definition Pattern

pub struct {Language}Parser {
    parser: Parser,                    // tree-sitter Parser
    context: ParserContext,            // Scope tracking (module-level, class, function)
    node_tracker: NodeTrackingState,   // ABI-15 audit tracking

    // Language-specific state (if needed)
    default_exported_symbols: HashSet<String>,  // TypeScript: default exports
    component_usages: Vec<(String, String)>,    // TypeScript: JSX tracking
}

Guidelines:

  • Use ParserContext for scope tracking (current class, function, module)
  • Use NodeTrackingState for automatic audit tracking
  • Add language-specific collections only when needed
  • Document all state fields with inline comments

Method Organization & Naming

1. Core Public API (Required by LanguageParser trait)

impl {Language}Parser {
    // Constructor
    pub fn new() -> Result<Self, String> { }

    // Main entry point - implements LanguageParser::parse()
    pub fn parse(&mut self, code: &str, file_id: FileId, counter: &mut SymbolCounter) -> Vec<Symbol> { }
}

2. Primary Internal Methods (Private, core logic)

Pattern: Use descriptive prefixes to indicate method purpose

// Symbol extraction router (recursive)
fn extract_symbols_from_node(
    &mut self,
    node: Node,
    code: &str,
    file_id: FileId,
    counter: &mut SymbolCounter,
    symbols: &mut Vec<Symbol>,
    module_path: &str,
    depth: usize,
) { }

// Symbol processors - one per major symbol type
fn process_function(...) -> Option<Symbol> { }
fn process_class(...) -> Option<Symbol> { }
fn process_interface(...) -> Option<Symbol> { }
fn process_type_alias(...) -> Option<Symbol> { }
fn process_enum(...) -> Option<Symbol> { }
fn process_method(...) -> Option<Symbol> { }
fn process_property(...) -> Option<Symbol> { }
fn process_variable_declaration(...) { }

// Specialized extractors for complex structures
fn extract_class_members(...) { }
fn extract_imports_from_node(...) { }

Naming Convention:

  • extract_* → Recursive traversal that populates a collection
  • process_* → Converts a single node to a Symbol (returns Option)
  • find_* → Public trait methods that search for relationships

3. Helper Methods (Utilities)

// Symbol creation helper (reduces boilerplate)
fn create_symbol(
    &self,
    id: SymbolId,
    name: String,
    kind: SymbolKind,
    file_id: FileId,
    range: Range,
    signature: Option<String>,
    doc_comment: Option<String>,
    module_path: &str,
    visibility: Visibility,
) -> Symbol { }

// Signature extraction (exclude body)
fn extract_signature(&self, node: Node, code: &str) -> String { }
fn extract_class_signature(&self, node: Node, code: &str) -> String { }
fn extract_interface_signature(&self, node: Node, code: &str) -> String { }

// Visibility determination
fn determine_visibility(&self, node: Node, code: &str) -> Visibility { }
fn determine_method_visibility(&self, node: Node, code: &str) -> Visibility { }

// Type extraction helpers
fn extract_type_name<'a>(&self, node: Node, code: &'a str) -> Option<&'a str> { }
fn extract_simple_type_name<'a>(&self, node: Node, code: &'a str) -> Option<&'a str> { }

Pattern: Helper methods follow consistent naming:

  • extract_{noun} → Extracts a specific piece of data
  • determine_{noun} → Makes a decision based on heuristics
  • create_{noun} → Factory method for construction

4. Relationship Extraction (Recursive traversal)

// Find relationships - implements LanguageParser trait methods
fn find_calls<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { }
fn find_method_calls(&mut self, code: &str) -> Vec<MethodCall> { }
fn find_implementations<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { }
fn find_extends<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { }
fn find_uses<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { }
fn find_defines<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> { }
fn find_imports(&mut self, code: &str, file_id: FileId) -> Vec<Import> { }

// Internal recursive helpers
fn extract_calls_recursive<'a>(...) { }
fn extract_method_calls_recursive(...) { }
fn extract_type_uses_recursive<'a>(...) { }
fn extract_method_defines_recursive<'a>(...) { }

Pattern:

  • Public find_* methods call private extract_*_recursive helpers
  • Use lifetime 'a for zero-copy string slices from source code
  • Return tuples for simple relationships, structured types for complex ones

5. Node Tracking (ABI-15 audit support)

// Register node for audit tracking
fn register_node_recursively(&mut self, node: Node) {
    self.register_handled_node(node.kind(), node.kind_id());
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        self.register_node_recursively(child);
    }
}

When to use:

  • Call register_node_recursively(node) for major symbols (functions, classes)
  • Call register_handled_node(kind, id) for individual nodes in switch cases

Implementation Pattern: extract_symbols_from_node

This is the heart of every parser. It's a recursive switch statement that routes nodes to their processors.

fn extract_symbols_from_node(
    &mut self,
    node: Node,
    code: &str,
    file_id: FileId,
    counter: &mut SymbolCounter,
    symbols: &mut Vec<Symbol>,
    module_path: &str,
    depth: usize,
) {
    // ALWAYS guard against stack overflow
    if !check_recursion_depth(depth, node) {
        return;
    }

    match node.kind() {
        // Major symbol types - register ALL children, then process
        "function_declaration" | "generator_function_declaration" => {
            self.register_node_recursively(node);

            // Extract name for parent tracking
            let func_name = node.child_by_field_name("name")
                .map(|n| code[n.byte_range()].to_string());

            // Process and add symbol
            if let Some(symbol) = self.process_function(node, code, file_id, counter, module_path) {
                symbols.push(symbol);
            }

            // Enter scope and set parent context
            self.context.enter_scope(ScopeType::hoisting_function());
            let saved_function = self.context.current_function().map(|s| s.to_string());
            let saved_class = self.context.current_class().map(|s| s.to_string());
            self.context.set_current_function(func_name.clone());

            // Process body for nested symbols
            if let Some(body) = node.child_by_field_name("body") {
                self.register_handled_node(body.kind(), body.kind_id());
                self.extract_symbols_from_node(body, code, file_id, counter, symbols, module_path, depth + 1);
            }

            // Exit scope, then restore parent context
            self.context.exit_scope();
            self.context.set_current_function(saved_function);
            self.context.set_current_class(saved_class);
        }

        "class_declaration" | "abstract_class_declaration" => {
            self.register_node_recursively(node);

            let class_name = node.children(&mut node.walk())
                .find(|n| n.kind() == "type_identifier")
                .map(|n| code[n.byte_range()].to_string());

            if let Some(symbol) = self.process_class(node, code, file_id, counter, module_path) {
                symbols.push(symbol);
                self.context.enter_scope(ScopeType::Class);

                let saved_function = self.context.current_function().map(|s| s.to_string());
                let saved_class = self.context.current_class().map(|s| s.to_string());
                self.context.set_current_class(class_name.clone());

                // Extract members in class scope
                self.extract_class_members(node, code, file_id, counter, symbols, module_path, depth + 1);

                self.context.exit_scope();
                self.context.set_current_function(saved_function);
                self.context.set_current_class(saved_class);
            }
        }

        // Simple symbol types - register and process
        "interface_declaration" => {
            self.register_node_recursively(node);
            if let Some(symbol) = self.process_interface(node, code, file_id, counter, module_path) {
                symbols.push(symbol);
            }
        }

        // Structural nodes - register and recurse
        "export_statement" => {
            self.register_handled_node(node.kind(), node.kind_id());

            // Language-specific export handling (e.g., default exports in TypeScript)
            // ... custom logic ...

            // Recurse to children
            let mut cursor = node.walk();
            for child in node.children(&mut cursor) {
                self.extract_symbols_from_node(child, code, file_id, counter, symbols, module_path, depth + 1);
            }
        }

        // ERROR nodes - special handling for parser recovery
        "ERROR" => {
            self.register_handled_node(node.kind(), node.kind_id());

            // Attempt to extract symbols from ERROR node children
            // This handles partial parses (e.g., "use client" directive in React)
            let mut cursor = node.walk();
            for child in node.children(&mut cursor) {
                self.extract_symbols_from_node(child, code, file_id, counter, symbols, module_path, depth + 1);
            }
        }

        // Default case - track and recurse
        _ => {
            self.register_handled_node(node.kind(), node.kind_id());
            let mut cursor = node.walk();
            for child in node.children(&mut cursor) {
                self.extract_symbols_from_node(child, code, file_id, counter, symbols, module_path, depth + 1);
            }
        }
    }
}

Critical Patterns:

  1. Always check recursion depth first
  2. Always register nodes for audit tracking
  3. Save and restore parent context when entering/exiting scopes
  4. Exit scope BEFORE restoring parent context
  5. Recurse to children for unhandled node types

Implementation Pattern: process_* Methods

These methods convert a single AST node into a Symbol. They follow a consistent structure:

fn process_function(
    &mut self,
    node: Node,
    code: &str,
    file_id: FileId,
    counter: &mut SymbolCounter,
    module_path: &str,
) -> Option<Symbol> {
    // 1. Extract name (early return if not found)
    let name_node = node.child_by_field_name("name")?;
    let name = &code[name_node.byte_range()];

    // 2. Extract metadata
    let signature = self.extract_signature(node, code);
    let doc_comment = self.extract_doc_comment(&node, code);
    let visibility = self.determine_visibility(node, code);

    // 3. Create and return symbol
    Some(self.create_symbol(
        counter.next_id(),
        name.to_string(),
        SymbolKind::Function,
        file_id,
        Range::new(
            node.start_position().row as u32,
            node.start_position().column as u16,
            node.end_position().row as u32,
            node.end_position().column as u16,
        ),
        Some(signature),
        doc_comment,
        module_path,
        visibility,
    ))
}

Pattern:

  1. Extract name (use ? for early return)
  2. Extract signature, doc, visibility
  3. Call create_symbol helper
  4. Return Option<Symbol>

Signature Extraction Pattern

Goal: Extract declaration without the body

fn extract_signature(&self, node: Node, code: &str) -> String {
    let start = node.start_byte();
    let mut end = node.end_byte();

    // Exclude body if present
    if let Some(body) = node.child_by_field_name("body") {
        end = body.start_byte();
    }

    code[start..end].trim().to_string()
}

Specialized versions: Create separate methods for complex signatures

  • extract_class_signature - Include extends/implements
  • extract_interface_signature - Include generic constraints
  • extract_method_signature - Include parameters only

Visibility Determination Pattern

Heuristic-based: Check multiple locations for visibility modifiers

fn determine_visibility(&self, node: Node, code: &str) -> Visibility {
    // 1. Ancestor check (wrapper nodes)
    let mut anc = node.parent();
    for _ in 0..3 {  // Check up to 3 levels
        if let Some(a) = anc {
            if a.kind() == "export_statement" {
                return Visibility::Public;
            }
            anc = a.parent();
        } else {
            break;
        }
    }

    // 2. Sibling check
    if let Some(prev) = node.prev_sibling() {
        if prev.kind() == "export_statement" {
            return Visibility::Public;
        }
    }

    // 3. Token check (inline modifiers)
    let start = node.start_byte();
    let prefix = safe_substring_window(code, start, 10);
    if prefix.contains("export ") || prefix.contains("export\n") {
        return Visibility::Public;
    }

    // Default
    Visibility::Private
}

Pattern: Check multiple locations because tree-sitter grammars vary

  1. Ancestor nodes (most common)
  2. Sibling nodes (rare)
  3. Source text tokens (fallback)

Behavior Implementation (behavior.rs)

Struct Definition Pattern

#[derive(Clone)]
pub struct {Language}Behavior {
    state: BehaviorState,  // Thread-safe state from base module
}

impl {Language}Behavior {
    pub fn new() -> Self {
        Self {
            state: BehaviorState::new(),
        }
    }
}

impl Default for {Language}Behavior {
    fn default() -> Self {
        Self::new()
    }
}

impl StatefulBehavior for {Language}Behavior {
    fn state(&self) -> &BehaviorState {
        &self.state
    }
}

Always:

  • Implement Clone, Default
  • Implement StatefulBehavior to access shared state
  • Use BehaviorState for imports, files, trait impls tracking

Required LanguageBehavior Methods

1. Module Path Formatting

fn format_module_path(&self, base_path: &str, symbol_name: &str) -> String {
    // Language conventions:
    // Rust: "crate::module::Symbol"
    // TypeScript: "module/path" (file-based, no symbol name)
    // Python: "package.module.Symbol"
    // Go: "module/submodule"
}

fn module_separator(&self) -> &'static str {
    // Rust: "::"
    // TypeScript/Python: "."
    // PHP: "\\"
    // Go: "/"
}

fn module_path_from_file(&self, file_path: &Path, project_root: &Path) -> Option<String> {
    // Convert file path to module path
    // Remove common prefixes (src/, lib/)
    // Remove file extensions
    // Replace path separators with module separators
}

2. Visibility Parsing

fn parse_visibility(&self, signature: &str) -> Visibility {
    // Check signature for visibility keywords
    // Language-specific rules:
    // - Rust: pub, pub(crate), pub(super), (none)
    // - TypeScript: export, private, protected, (none)
    // - Python: (none), _private, __very_private
    // - Go: Uppercase = public, lowercase = private
}

3. Language Capabilities

fn supports_traits(&self) -> bool {
    // Rust: true (trait system)
    // TypeScript: true (interfaces)
    // Python: false (duck typing)
    // Go: true (interfaces)
}

fn supports_inherent_methods(&self) -> bool {
    // Rust: true (impl Block)
    // TypeScript: false (methods defined in class)
    // Python: true (methods in class body)
}

fn get_language(&self) -> Language {
    tree_sitter_{language}::LANGUAGE_{LANGUAGE}.into()
}

4. Resolution Context Creation

fn create_resolution_context(&self, file_id: FileId) -> Box<dyn ResolutionScope> {
    Box::new({Language}ResolutionContext::new(file_id))
}

fn create_inheritance_resolver(&self) -> Box<dyn InheritanceResolver> {
    Box::new({Language}InheritanceResolver::new())
}

Advanced Resolution Methods

Import Resolution

fn resolve_import(
    &self,
    import: &Import,
    document_index: &DocumentIndex,
) -> Option<SymbolId> {
    // 1. Get importing file's module path
    let importing_mod = self.get_module_path_for_file(import.file_id)?;

    // 2. Normalize import path (relative -> absolute)
    let normalized = normalize_import_path(&import.path, &importing_mod);

    // 3. Search for matching symbols
    let candidates = document_index.find_symbols_by_name(&import.path, None).ok()?;

    // 4. Filter by module path match
    for candidate in candidates {
        if self.import_matches_symbol(&normalized, &candidate) {
            return Some(candidate.id);
        }
    }

    None
}

fn import_matches_symbol(&self, import_path: &str, candidate: &Symbol) -> bool {
    // Language-specific matching rules
    // - Handle relative paths
    // - Handle aliases
    // - Handle index files
    // - Handle extensions
}

Heuristics: This is where you encode language-specific import semantics

External Call Resolution

fn resolve_external_call_target(
    &self,
    call_target: &str,
    context_symbol: &Symbol,
    document_index: &DocumentIndex,
) -> Option<SymbolId> {
    // Pattern: Try multiple resolution strategies

    // 1. Check if it's a qualified name (e.g., "Namespace.function")
    if let Some(symbol_id) = self.resolve_qualified_name(call_target, context_symbol, document_index) {
        return Some(symbol_id);
    }

    // 2. Check imports in caller's file
    let imports = self.state.get_imports_for_file(context_symbol.file_id);
    for import in imports {
        if import.alias.as_deref() == Some(call_target) || import.path.ends_with(call_target) {
            return self.resolve_import(&import, document_index);
        }
    }

    // 3. Check parent modules (for relative imports)
    if let Some(symbol_id) = self.resolve_in_parent_modules(call_target, context_symbol, document_index) {
        return Some(symbol_id);
    }

    None
}

Pattern: Try multiple strategies, return first match

State Management Methods

// StatefulBehavior provides these through BehaviorState:

fn add_import(&self, import: Import) {
    self.state.add_import(import);
}

fn register_file(&self, path: PathBuf, file_id: FileId, module_path: String) {
    self.state.register_file(path, file_id, module_path);
}

fn add_trait_impl(&self, type_name: String, trait_name: String, file_id: FileId) {
    self.state.add_trait_impl(type_name, trait_name, file_id);
}

fn get_imports_for_file(&self, file_id: FileId) -> Vec<Import> {
    self.state.get_imports_for_file(file_id)
}

fn get_module_path_for_file(&self, file_id: FileId) -> Option<String> {
    self.state.get_file_info(file_id).map(|info| info.module_path.clone())
}

Don't reinvent: Use BehaviorState for standard tracking


Resolution Implementation (resolution.rs)

Struct Definition Pattern

pub struct {Language}ResolutionContext {
    file_id: FileId,

    // Scoped symbol maps (from inner to outer)
    local_scope: HashMap<String, SymbolId>,
    module_symbols: HashMap<String, SymbolId>,
    imported_symbols: HashMap<String, SymbolId>,
    global_symbols: HashMap<String, SymbolId>,

    // Scope stack for tracking nesting
    scope_stack: Vec<ScopeType>,

    // Import tracking
    imports: Vec<(String, Option<String>)>,
    import_bindings: HashMap<String, ImportBinding>,

    // Language-specific state
    // TypeScript: hoisted_scope, type_space, qualified_names, namespace_aliases
    // Rust: crate_scope, use_statements, trait_impls
    // Python: LEGB scopes, __all__ tracking
}

Guidelines:

  • Organize scopes from innermost to outermost
  • Use HashMap<String, SymbolId> for symbol lookups
  • Track imports separately from symbols
  • Add language-specific maps as needed

Required ResolutionScope Methods

Symbol Resolution (Core)

fn resolve(&self, name: &str) -> Option<SymbolId> {
    // Language-specific resolution order
    // TypeScript: local -> hoisted -> imported -> module -> global
    // Rust: local -> imported -> module -> crate -> global
    // Python: Local -> Enclosing -> Global -> Built-in (LEGB)

    // 1. Check local scope
    if let Some(id) = self.local_scope.get(name) {
        return Some(*id);
    }

    // 2. Check hoisted scope (TypeScript, JavaScript)
    if let Some(id) = self.hoisted_scope.get(name) {
        return Some(*id);
    }

    // 3. Check imports
    if let Some(id) = self.imported_symbols.get(name) {
        return Some(*id);
    }

    // 4. Check module scope
    if let Some(id) = self.module_symbols.get(name) {
        return Some(*id);
    }

    // 5. Check global scope
    self.global_symbols.get(name).copied()
}

Pattern: Implement language-specific scope precedence

Symbol Addition

fn add_symbol(&mut self, name: String, symbol_id: SymbolId, scope_level: ScopeLevel) {
    match scope_level {
        ScopeLevel::Local => {
            self.local_scope.insert(name, symbol_id);
        }
        ScopeLevel::Module => {
            self.module_symbols.insert(name, symbol_id);
        }
        ScopeLevel::Imported => {
            self.imported_symbols.insert(name, symbol_id);
        }
        ScopeLevel::Global => {
            self.global_symbols.insert(name, symbol_id);
        }
    }
}

Scope Management

Basic scope operations:

fn enter_scope(&mut self, scope_type: ScopeType) {
    self.scope_stack.push(scope_type);
}

fn exit_scope(&mut self) {
    if self.scope_stack.pop().is_some() {
        // Clear local scope on exit
        self.local_scope.clear();
    }
}

fn clear_local_scope(&mut self) {
    self.local_scope.clear();
}

CRITICAL PATTERN: Save → Enter → Process → Exit → Restore

This is the most important pattern for correct scoping. Always save parent context before entering a new scope:

// In extract_symbols_from_node, when processing a function:

"function_declaration" => {
    // 1. SAVE current parent context
    let saved_function = self.context.current_function().map(|s| s.to_string());
    let saved_class = self.context.current_class().map(|s| s.to_string());

    // 2. ENTER new scope
    self.context.enter_scope(ScopeType::Function { hoisting: true });

    // 3. SET new parent context (for symbols inside this function)
    self.context.set_current_function(Some(function_name));

    // 4. PROCESS child symbols
    if let Some(body) = node.child_by_field_name("body") {
        self.extract_symbols_from_node(body, code, file_id, counter, symbols, module_path, depth + 1);
    }

    // 5. EXIT scope FIRST (this clears local_scope)
    self.context.exit_scope();

    // 6. RESTORE parent context AFTER exit
    self.context.set_current_function(saved_function);
    self.context.set_current_class(saved_class);
}

Why this order matters:

  1. Save first: Captures current context before modification
  2. Enter scope: Pushes new scope onto stack
  3. Set context: Updates current_function/current_class for children
  4. Process: Child symbols get correct parent_function/parent_class
  5. Exit BEFORE restore: Clears scope data structures
  6. Restore AFTER exit: Returns to previous context

Common mistake:

// WRONG - restoring before exit
self.context.set_current_function(saved_function);  // Restores first
self.context.exit_scope();                           // Then exits - BAD!

This causes the restored context to be cleared by exit_scope()!

Relationship Resolution

fn resolve_relationship(
    &self,
    target_name: &str,
    context: &Symbol,
    relation_kind: RelationKind,
    document_index: &DocumentIndex,
) -> Option<SymbolId> {
    match relation_kind {
        RelationKind::Calls => {
            // Try local resolution first
            if let Some(id) = self.resolve(target_name) {
                return Some(id);
            }

            // Fall back to document index
            self.resolve_external_call(target_name, context, document_index)
        }
        RelationKind::Implements | RelationKind::Extends => {
            // Type names are usually qualified
            self.resolve_type_name(target_name, document_index)
        }
        RelationKind::Uses => {
            // Types used in parameters, fields, returns
            self.resolve_type_name(target_name, document_index)
        }
        _ => None,
    }
}

Inheritance Resolver Pattern

pub struct {Language}InheritanceResolver {
    // Track inheritance relationships
    inheritance_map: HashMap<String, Vec<String>>,  // child -> parents

    // Track methods defined by types
    type_methods: HashMap<String, Vec<String>>,

    // Language-specific data
    // TypeScript: interface_map, class_extends, class_implements
    // Rust: trait_impls, inherent_impls
}

impl InheritanceResolver for {Language}InheritanceResolver {
    fn is_interface(&self, type_name: &str) -> bool {
        // Language-specific interface detection
    }

    fn add_inheritance(&mut self, child: String, parent: String, kind: &str) {
        // Track inheritance (extends vs implements)
    }

    fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<String> {
        // Walk inheritance chain to find method provider
        let chain = self.get_inheritance_chain(type_name);
        for ancestor in chain {
            if self.type_has_method(&ancestor, method_name) {
                return Some(ancestor);
            }
        }
        None
    }

    fn get_inheritance_chain(&self, type_name: &str) -> Vec<String> {
        // BFS or DFS traversal of inheritance graph
    }

    fn is_subtype(&self, child: &str, parent: &str) -> bool {
        // Check if child inherits from parent
    }
}

Definition & Registration (definition.rs)

Simplest file - just implements the trait and provides registration.

//! {Language} language definition and registration

use crate::parsing::{
    LanguageBehavior, LanguageDefinition, LanguageId, LanguageParser, LanguageRegistry,
};
use crate::{IndexError, IndexResult, Settings};
use std::sync::Arc;

use super::{Language}Behavior, {Language}Parser};

/// {Language} language definition
pub struct {Language}Language;

impl LanguageDefinition for {Language}Language {
    fn id(&self) -> LanguageId {
        LanguageId::new("{language}")  // lowercase
    }

    fn name(&self) -> &'static str {
        "{Language}"  // proper case
    }

    fn extensions(&self) -> &'static [&'static str] {
        // TypeScript: &["ts", "tsx", "mts", "cts"]
        // Rust: &["rs"]
        // Python: &["py", "pyi"]
        // Go: &["go"]
        &["ext1", "ext2"]
    }

    fn create_parser(&self, _settings: &Settings) -> IndexResult<Box<dyn LanguageParser>> {
        let parser = {Language}Parser::new()
            .map_err(|e| IndexError::General(e.to_string()))?;
        Ok(Box::new(parser))
    }

    fn create_behavior(&self) -> Box<dyn LanguageBehavior> {
        Box::new({Language}Behavior::new())
    }

    fn default_enabled(&self) -> bool {
        true  // or false for less common languages
    }

    fn is_enabled(&self, settings: &Settings) -> bool {
        settings
            .languages
            .get("{language}")
            .map(|config| config.enabled)
            .unwrap_or(self.default_enabled())
    }
}

/// Register {Language} language with the registry
pub(crate) fn register(registry: &mut LanguageRegistry) {
    registry.register(Arc::new({Language}Language));
}

Then register in src/parsing/registry.rs:initialize_registry():

fn initialize_registry(registry: &mut LanguageRegistry) {
    super::rust::register(registry);
    super::typescript::register(registry);
    super::{language}::register(registry);  // Add your language
}

Module Exports (mod.rs)

//! {Language} language parser implementation

pub mod audit;
pub mod behavior;
pub mod definition;
pub mod parser;
pub mod resolution;

pub use behavior::{Language}Behavior;
pub use definition::{Language}Language;
pub use parser::{Language}Parser;
pub use resolution::{Language}InheritanceResolver, {Language}ResolutionContext};

// Re-export for registry registration
pub(crate) use definition::register;

Then update src/parsing/mod.rs:

pub mod {language};

pub use {language}::{Language}Behavior, {Language}Parser, {Language}Language};

Audit System (audit.rs)

Copy-paste template - minimal customization needed.

//! {Language} parser audit module

use super::{Language}Parser;
use crate::io::format::format_utc_timestamp;
use crate::parsing::NodeTracker;
use crate::types::FileId;
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use tree_sitter::{Node, Parser};

#[derive(Error, Debug)]
pub enum AuditError {
    #[error("Failed to read file: {0}")]
    FileRead(#[from] std::io::Error),

    #[error("Failed to set language: {0}")]
    LanguageSetup(String),

    #[error("Failed to parse code")]
    ParseFailure,

    #[error("Failed to create parser: {0}")]
    ParserCreation(String),
}

pub struct {Language}ParserAudit {
    pub grammar_nodes: HashMap<String, u16>,
    pub implemented_nodes: HashSet<String>,
    pub extracted_symbol_kinds: HashSet<String>,
}

impl {Language}ParserAudit {
    pub fn audit_file(file_path: &str) -> Result<Self, AuditError> {
        let code = std::fs::read_to_string(file_path)?;
        Self::audit_code(&code)
    }

    pub fn audit_code(code: &str) -> Result<Self, AuditError> {
        // Parse with tree-sitter to discover all nodes
        let mut parser = Parser::new();
        let language = tree_sitter_{language}::LANGUAGE_{LANGUAGE}.into();
        parser.set_language(&language)
            .map_err(|e| AuditError::LanguageSetup(e.to_string()))?;

        let tree = parser.parse(code, None).ok_or(AuditError::ParseFailure)?;

        let mut grammar_nodes = HashMap::new();
        discover_nodes(tree.root_node(), &mut grammar_nodes);

        // Parse with our parser to see what symbols get extracted
        let mut lang_parser = {Language}Parser::new()
            .map_err(|e| AuditError::ParserCreation(e.to_string()))?;
        let file_id = FileId(1);
        let mut symbol_counter = crate::types::SymbolCounter::new();
        let symbols = lang_parser.parse(code, file_id, &mut symbol_counter);

        let mut extracted_symbol_kinds = HashSet::new();
        for symbol in &symbols {
            extracted_symbol_kinds.insert(format!("{:?}", symbol.kind));
        }

        let implemented_nodes: HashSet<String> = lang_parser
            .get_handled_nodes()
            .iter()
            .map(|handled_node| handled_node.name.clone())
            .collect();

        Ok(Self {
            grammar_nodes,
            implemented_nodes,
            extracted_symbol_kinds,
        })
    }

    pub fn generate_report(&self) -> String {
        let mut report = String::new();

        report.push_str("# {Language} Parser Coverage Report\n\n");
        report.push_str(&format!("*Generated: {}*\n\n", format_utc_timestamp()));

        // Summary
        report.push_str("## Summary\n");
        report.push_str(&format!("- Nodes in file: {}\n", self.grammar_nodes.len()));
        report.push_str(&format!("- Nodes handled: {}\n", self.implemented_nodes.len()));

        let coverage = if self.grammar_nodes.is_empty() {
            0.0
        } else {
            (self.implemented_nodes.len() as f64 / self.grammar_nodes.len() as f64) * 100.0
        };
        report.push_str(&format!("- Coverage: {:.1}%\n\n", coverage));

        // ... more reporting ...

        report
    }
}

fn discover_nodes(node: Node, nodes: &mut HashMap<String, u16>) {
    nodes.insert(node.kind().to_string(), node.kind_id());
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        discover_nodes(child, nodes);
    }
}

Naming Conventions Reference

Method Prefixes

PrefixPurposeReturnsExample
extract_*Recursive traversal, populates collectionvoid or collectionextract_symbols_from_node, extract_calls_recursive
process_*Convert single node to symbolOption<Symbol>process_function, process_class
find_*Public API for relationship searchVec<T>find_calls, find_implementations
determine_*Heuristic-based decisionEnum valuedetermine_visibility, determine_method_visibility
resolve_*Symbol/path resolutionOption<SymbolId>resolve_import, resolve_external_call
register_*Track state or metadatavoidregister_node_recursively, register_file
create_*Factory methodConstructed typecreate_symbol, create_resolution_context

Struct Field Patterns

FieldTypePurpose
parserParsertree-sitter parser instance
contextParserContextScope tracking (function, class, module)
node_trackerNodeTrackingStateABI-15 audit tracking
stateBehaviorStateShared state (imports, files, traits)
{scope}_scopeHashMap<String, SymbolId>Symbol lookup by scope level
scope_stackVec<ScopeType>Nested scope tracking
importsVec<Import> or Vec<(String, Option<String>)>Import statements
import_bindingsHashMap<String, ImportBinding>Resolved import metadata

Common Patterns & Heuristics

1. Recursion Guard

Always protect against stack overflow in recursive methods:

fn extract_symbols_from_node(..., depth: usize) {
    if !check_recursion_depth(depth, node) {
        return;
    }
    // ... rest of method ...
    self.extract_symbols_from_node(..., depth + 1);
}

2. Parent Context Tracking

Pattern: Save, set, restore parent context when entering nested scopes

// Save current context
let saved_function = self.context.current_function().map(|s| s.to_string());
let saved_class = self.context.current_class().map(|s| s.to_string());

// Set new context
self.context.set_current_function(new_function_name);
self.context.set_current_class(new_class_name);

// Process children
self.extract_symbols_from_node(...);

// Exit scope FIRST
self.context.exit_scope();

// Then restore context
self.context.set_current_function(saved_function);
self.context.set_current_class(saved_class);

Order matters: Exit scope before restoring context.

3. ERROR Node Handling

Tree-sitter produces ERROR nodes when it can't parse something. Don't ignore them:

"ERROR" => {
    self.register_handled_node(node.kind(), node.kind_id());

    // Try to extract symbols from children anyway
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        self.extract_symbols_from_node(child, ...);
    }
}

Example: React's "use client" directive causes ERROR nodes, but we still extract components.

4. Multi-Strategy Resolution

When resolving imports or calls, try multiple strategies:

fn resolve_external_call(...) -> Option<SymbolId> {
    // 1. Try qualified name resolution
    if let Some(id) = self.resolve_qualified_name(...) {
        return Some(id);
    }

    // 2. Try import resolution
    if let Some(id) = self.resolve_via_imports(...) {
        return Some(id);
    }

    // 3. Try parent module resolution
    if let Some(id) = self.resolve_in_parent_modules(...) {
        return Some(id);
    }

    // 4. Give up
    None
}

5. Visibility Heuristics

Check multiple locations because tree-sitter grammars differ:

fn determine_visibility(...) -> Visibility {
    // 1. Ancestor check (export_statement wrapper)
    // 2. Sibling check (adjacent export token)
    // 3. Source text check (inline 'export' keyword)
    // Default: Private
}

6. Scope Precedence

Implement language-specific scope resolution order:

// TypeScript: local -> hoisted -> imported -> module -> global
// Rust: local -> imported -> module -> crate -> global
// Python: Local -> Enclosing -> Global -> Built-in (LEGB)

fn resolve(&self, name: &str) -> Option<SymbolId> {
    self.local_scope.get(name)
        .or_else(|| self.hoisted_scope.get(name))   // TypeScript only
        .or_else(|| self.imported_symbols.get(name))
        .or_else(|| self.module_symbols.get(name))
        .or_else(|| self.global_symbols.get(name))
        .copied()
}

7. Import Path Normalization

Convert relative imports to absolute before matching:

fn normalize_import_path(import_path: &str, importing_module: &str) -> String {
    if import_path.starts_with("./") || import_path.starts_with("../") {
        // Resolve relative to importing module
        resolve_relative_path(import_path, importing_module)
    } else {
        // Already absolute
        import_path.to_string()
    }
}

fn resolve_relative_path(import: &str, base: &str) -> String {
    let base_parts: Vec<&str> = base.split('.').collect();
    let mut result = base_parts[..base_parts.len()-1].to_vec();

    for segment in import.split('.') {
        if segment == ".." {
            result.pop();
        } else if segment != "." {
            result.push(segment);
        }
    }

    result.join(".")
}

8. Zero-Copy String Slices

Use lifetimes for efficient string extraction:

fn find_calls<'a>(&mut self, code: &'a str) -> Vec<(&'a str, &'a str, Range)> {
    let mut calls = Vec::new();
    self.extract_calls_recursive(tree.root_node(), code, &mut calls);
    calls
}

fn extract_calls_recursive<'a>(
    &self,
    node: Node,
    code: &'a str,
    calls: &mut Vec<(&'a str, &'a str, Range)>,
) {
    // Extract string slices without copying
    let caller = &code[caller_node.byte_range()];
    let callee = &code[callee_node.byte_range()];
    calls.push((caller, callee, range));
}

Benefit: No string allocations, just pointers into source.

9. Node Registration for Auditing

Register nodes as you encounter them:

// Major symbols - register entire subtree
"function_declaration" => {
    self.register_node_recursively(node);
    // ... process function ...
}

// Individual nodes - register once
"export_statement" => {
    self.register_handled_node(node.kind(), node.kind_id());
    // ... process exports ...
}

// Default case - track all nodes
_ => {
    self.register_handled_node(node.kind(), node.kind_id());
    // ... recurse to children ...
}

Result: Automatic coverage tracking, no manual maintenance.

10. Signature Without Body

Extract declaration, exclude implementation:

fn extract_signature(&self, node: Node, code: &str) -> String {
    let start = node.start_byte();
    let mut end = node.end_byte();

    // Find body node and exclude it
    if let Some(body) = node.child_by_field_name("body") {
        end = body.start_byte();
    }

    code[start..end].trim().to_string()
}

Examples:

  • Function: function foo(x: number): string (exclude { ... })
  • Class: class Foo extends Bar implements Baz (exclude { ... })
  • Interface: interface IFoo extends IBar (exclude { ... })

Step-by-Step Implementation Checklist

Phase 1: Setup & ABI-15 Exploration

  • Create language directory: src/parsing/{language}/
  • Add dependency to Cargo.toml: tree-sitter-{language} = "0.x"
  • Create comprehensive test in tests/abi15_exploration.rs
  • Document ABI-15 findings in contributing/parsers/{language}/NODE_MAPPING.md

Phase 2: Core Files

  • Create mod.rs with public exports
  • Create definition.rs and implement LanguageDefinition
  • Create parser.rs with struct and new() method
  • Create behavior.rs with struct and new() method
  • Create resolution.rs with context and inheritance structs
  • Create audit.rs (copy from TypeScript, adjust language name)

Phase 3: Parser Implementation

  • Implement extract_symbols_from_node switch statement
  • Implement process_* methods for each symbol type
  • Implement extract_signature methods
  • Implement determine_visibility methods
  • Implement find_calls, find_imports, etc. (LanguageParser trait)
  • Add tests for each major feature

Phase 4: Behavior Implementation

  • Implement format_module_path and module_separator
  • Implement module_path_from_file
  • Implement parse_visibility
  • Implement create_resolution_context and create_inheritance_resolver
  • Implement resolve_import (if needed)
  • Add tests for resolution

Phase 5: Resolution Implementation

  • Implement resolve method with language-specific scope order
  • Implement add_symbol with scope levels
  • Implement resolve_relationship for different relation kinds
  • Implement inheritance resolver methods
  • Add tests for scoping rules

Phase 6: Registration & Integration

  • Register in src/parsing/registry.rs:initialize_registry()
  • Export in src/parsing/mod.rs
  • Add to .codanna/settings.toml default generation
  • Run full test suite: cargo test
  • Run audit: cargo test audit_{language} -- --nocapture

Phase 7: Documentation & Polish

  • Document language-specific heuristics
  • Add examples to examples/{language}/
  • Update contributing/development/language-support.md
  • Run cargo clippy and fix warnings
  • Run cargo fmt

Example Workflow: Adding a New Language

Let's say you're adding Java support.

1. Setup

# Add dependency
echo 'tree-sitter-java = "0.21"' >> Cargo.toml

# Create directory
mkdir -p src/parsing/java

# Create ABI-15 exploration test
cargo test explore_java_abi15_comprehensive -- --nocapture > contributing/parsers/java/node_discovery.txt

2. Create Files

touch src/parsing/java/{mod,definition,parser,behavior,resolution,audit}.rs

3. Implement parser.rs

use tree_sitter::{Language, Node, Parser};

pub struct JavaParser {
    parser: Parser,
    context: ParserContext,
    node_tracker: NodeTrackingState,
}

impl JavaParser {
    pub fn new() -> Result<Self, String> {
        let mut parser = Parser::new();
        let language: Language = tree_sitter_java::LANGUAGE.into();
        parser.set_language(&language)
            .map_err(|e| format!("Failed to set Java language: {e}"))?;

        Ok(Self {
            parser,
            context: ParserContext::new(),
            node_tracker: NodeTrackingState::new(),
        })
    }

    fn extract_symbols_from_node(...) {
        if !check_recursion_depth(depth, node) {
            return;
        }

        match node.kind() {
            "class_declaration" => {
                self.register_node_recursively(node);
                if let Some(symbol) = self.process_class(node, code, file_id, counter, module_path) {
                    symbols.push(symbol);
                }
            }
            "method_declaration" => {
                self.register_node_recursively(node);
                if let Some(symbol) = self.process_method(node, code, file_id, counter, module_path) {
                    symbols.push(symbol);
                }
            }
            // ... more cases from NODE_MAPPING.md
            _ => {
                self.register_handled_node(node.kind(), node.kind_id());
                for child in node.children(&mut node.walk()) {
                    self.extract_symbols_from_node(child, ...);
                }
            }
        }
    }

    fn process_class(...) -> Option<Symbol> {
        let name_node = node.child_by_field_name("name")?;
        let name = &code[name_node.byte_range()];

        let signature = self.extract_signature(node, code);
        let doc = self.extract_doc_comment(&node, code);
        let visibility = self.determine_visibility(node, code);

        Some(self.create_symbol(
            counter.next_id(),
            name.to_string(),
            SymbolKind::Class,
            file_id,
            Range::new(/* ... */),
            Some(signature),
            doc,
            module_path,
            visibility,
        ))
    }
}

4. Implement behavior.rs

pub struct JavaBehavior {
    state: BehaviorState,
}

impl JavaBehavior {
    pub fn new() -> Self {
        Self {
            state: BehaviorState::new(),
        }
    }
}

impl LanguageBehavior for JavaBehavior {
    fn format_module_path(&self, base_path: &str, symbol_name: &str) -> String {
        // Java: com.example.package.ClassName
        format!("{}.{}", base_path, symbol_name)
    }

    fn module_separator(&self) -> &'static str {
        "."  // Java uses dot separation
    }

    fn parse_visibility(&self, signature: &str) -> Visibility {
        if signature.contains("public ") {
            Visibility::Public
        } else if signature.contains("private ") {
            Visibility::Private
        } else if signature.contains("protected ") {
            Visibility::Module  // Map to Module
        } else {
            Visibility::Module  // Package-private (default)
        }
    }

    fn supports_traits(&self) -> bool {
        true  // Java has interfaces
    }

    fn get_language(&self) -> Language {
        tree_sitter_java::LANGUAGE.into()
    }
}

5. Implement resolution.rs

pub struct JavaResolutionContext {
    file_id: FileId,
    local_scope: HashMap<String, SymbolId>,
    class_scope: HashMap<String, SymbolId>,
    imported_symbols: HashMap<String, SymbolId>,
    package_scope: HashMap<String, SymbolId>,
    scope_stack: Vec<ScopeType>,
}

impl ResolutionScope for JavaResolutionContext {
    fn resolve(&self, name: &str) -> Option<SymbolId> {
        // Java resolution order: local -> class -> imported -> package
        self.local_scope.get(name)
            .or_else(|| self.class_scope.get(name))
            .or_else(|| self.imported_symbols.get(name))
            .or_else(|| self.package_scope.get(name))
            .copied()
    }

    // ... implement other required methods
}

6. Register

// src/parsing/registry.rs
fn initialize_registry(registry: &mut LanguageRegistry) {
    super::rust::register(registry);
    super::typescript::register(registry);
    super::java::register(registry);  // Add this
}

7. Test

cargo test java
cargo test audit_java -- --nocapture
cargo clippy --fix
cargo fmt

Done!


Summary

This guide provides the internal patterns and conventions used across all language implementations:

  1. File Organization: 6 files, each with a focused responsibility
  2. Method Naming: Consistent prefixes indicate purpose (extract_, process_, find_, determine_, resolve_)
  3. Implementation Patterns:
    • extract_symbols_from_node - recursive router
    • process_* - symbol constructors
    • determine_visibility - multi-location heuristics
    • resolve - scope precedence
  4. State Management: Use ParserContext, BehaviorState, resolution contexts
  5. Best Practices:
    • Recursion guards
    • Parent context tracking
    • ERROR node handling
    • Zero-copy string slices
    • Node registration for auditing

Follow these patterns for consistency, easier debugging, and maintainability across all language implementations.