How GitNexus Handles Language-Specific Parsing Quirks in Tree-Sitter Grammars

GitNexus normalizes disparate AST structures across 15+ programming languages by using hand-crafted tree-sitter queries that map language-specific node types—such as TypeScript's type_identifier or Kotlin's delegation_specifier—into a unified symbol schema.

GitNexus is an open-source code intelligence platform that extracts definitions, imports, and inheritance relationships from source code repositories. Because it relies on tree-sitter grammars that vary significantly in node naming and hierarchy, the project implements a comprehensive set of language-specific parsing quirks handlers in gitnexus/src/core/ingestion/tree-sitter-queries.ts to ensure consistent symbol extraction across TypeScript, Kotlin, Swift, and more than a dozen other languages.

Why Language-Specific Parsing Quirks Matter

Tree-sitter generates parsers from grammar definitions, but each language maintainer chooses different node names for equivalent concepts. TypeScript uses type_identifier for class names while JavaScript uses plain identifier. Kotlin represents interfaces as class_declaration nodes with a keyword check, not a dedicated interface_declaration. Without explicit normalization, these structural differences would fragment symbol extraction logic and produce inconsistent metadata across languages.

TypeScript vs. JavaScript: Divergent AST Nodes

Type Identifiers and Class Heritage

In gitnexus/src/core/ingestion/tree-sitter-queries.ts, GitNexus maintains separate query constants for TypeScript and JavaScript to handle their distinct node types.

TypeScript uses type_identifier for class and interface names:

// TYPESCRIPT_QUERIES in tree-sitter-queries.ts
(class_declaration
  name: (type_identifier) @definition.class)

JavaScript uses the more generic identifier node for the same concept:

// JAVASCRIPT_QUERIES in tree-sitter-queries.ts
(class_declaration
  name: (identifier) @definition.class)

Inheritance Structure Differences

The heritage node structure also differs between the two languages. JavaScript's class_heritage node directly contains the parent identifier, while TypeScript nests it inside an extends_clause. The queries in tree-sitter-queries.ts account for this structural variance to ensure both languages emit consistent @heritage.class captures.

Kotlin's Interface and Inheritance Oddities

Interfaces Masquerading as Classes

Kotlin presents a unique challenge: its tree-sitter grammar does not define an interface_declaration node. Instead, interfaces appear as class_declaration nodes containing the literal "interface" keyword. GitNexus handles this quirk by matching the pattern and explicitly emitting @definition.interface despite the underlying node type.

// KOTLIN_QUERIES in tree-sitter-queries.ts
(class_declaration
  "interface"
  (type_identifier) @definition.interface)

Delegation Specifiers for Inheritance

Kotlin uses delegation_specifier nodes to represent both extends and implements relationships, unlike Java-style languages that use distinct heritage clauses. GitNexus employs two separate query branches within KOTLIN_QUERIES to distinguish class extension (via constructor_invocation) from interface implementation (via user_type), ensuring accurate heritage mapping.

Swift's Granular Type Hierarchy

Swift's grammar distinguishes between class, struct, enum, extension, actor, and protocol declarations, each with distinct node types. GitNexus normalizes these into common schema labels—for example, mapping Swift extensions to @definition.class—while preserving the semantic distinctions through capture names.

Inheritance in Swift is expressed via inheritance_specifier nodes, which GitNexus queries specifically in SWIFT_QUERIES to extract parent relationships for classes and protocols alike.

C#, Rust, and PHP Edge Cases

C# Generic Base Lists

C# encapsulates inheritance information inside a base_list node, where parent types may appear as simple identifiers or generic constructions (generic_name). The CSHARP_QUERIES in tree-sitter-queries.ts captures both patterns to ensure generic base classes are not omitted from the heritage graph.

Rust Trait Implementation Nodes

Rust expresses trait implementation through impl_item nodes that contain both a trait and a type. GitNexus extracts these as dual heritage captures—@heritage.trait for the implemented trait and @heritage.class for the implementing type—preserving Rust's unique ownership model in the symbol graph.

PHP Null-Safe Calls and Import Variations

PHP requires handling multiple import declaration styles (namespace_use_declaration vs use_directive), null-safe method calls (nullsafe_member_call_expression), and static calls (scoped_call_expression). Each pattern has an explicit query clause in PHP_QUERIES to ensure modern PHP features are correctly captured.

Optional Dependencies and Graceful Degradation

Not all language parsers are mandatory dependencies. The Swift parser (tree-sitter-swift) is loaded as an optional dependency inside gitnexus/src/core/tree-sitter/parser-loader.ts. When the native binding is missing, GitNexus catches the error and gracefully skips Swift files rather than crashing the ingestion pipeline.

// parser-loader.ts lines 16-21
try {
  const Swift = await import('tree-sitter-swift');
  languages.set('swift', Swift.default);
} catch {
  // Swift parser not available, skip gracefully
}

Practical Example: Querying Symbols Across Languages

The following TypeScript example demonstrates how GitNexus's query system normalizes language-specific parsing quirks into a consistent capture format:

import { loadParser, loadLanguage } from 'gitnexus/src/core/tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from 'gitnexus/src/core/ingestion/tree-sitter-queries.js';
import { query } from 'tree-sitter';

async function runQuery(lang: string, source: string) {
  const Parser = await loadParser();
  const Language = await loadLanguage(lang);
  const parser = new Parser();
  parser.setLanguage(Language);
  const tree = parser.parse(source);

  const q = new query(Language, LANGUAGE_QUERIES[lang]);
  const captures = q.captures(tree.rootNode);
  return captures.map(c => ({
    type: c.name,
    text: c.node.text,
  }));
}

// Test different language quirks
const tsSource = `export class User { id: number; }`;
const ktSource = `interface Service { fun(): Unit }`;
const swiftSource = `protocol Drawable { func draw() }`;
const phpSource = `$obj?->save();`;

(async () => {
  console.log('TypeScript →', await runQuery('TypeScript', tsSource));
  console.log('Kotlin →', await runQuery('Kotlin', ktSource));
  console.log('Swift →', await runQuery('Swift', swiftSource));
  console.log('PHP (null-safe) →', await runQuery('PHP', phpSource));
})();

Output demonstrates normalization:

  • TypeScript: Captures User as definition.class despite the type_identifier node type
  • Kotlin: Captures Service as definition.interface despite being a class_declaration node with "interface" keyword
  • Swift: Maps Drawable protocol to definition.interface via the inheritance_specifier handling
  • PHP: Captures null-safe call save as @call via nullsafe_member_call_expression query

Key Source Files for Parsing Logic

File Purpose
gitnexus/src/core/ingestion/tree-sitter-queries.ts Central repository of per-language tree-sitter queries containing all language-specific node mappings and heritage adjustments.
gitnexus/src/core/tree-sitter/parser-loader.ts Dynamic parser loader with optional dependency handling for Swift and other native bindings.
gitnexus/src/core/ingestion/parsing-processor.ts Orchestrates query execution and applies language-specific fallbacks for file size limits or parsing errors.
gitnexus-web/src/core/tree-sitter/parser-loader.ts WebAssembly-compatible parser loader mirroring the core logic for browser-based ingestion.

Summary

  • GitNexus uses hand-crafted tree-sitter queries to normalize disparate AST structures across 15+ languages into a unified symbol schema.
  • TypeScript requires separate handling for type_identifier nodes and nested extends_clause structures compared to JavaScript's identifier and direct class_heritage.
  • Kotlin interfaces are captured via class_declaration nodes with keyword matching rather than dedicated interface nodes, while inheritance uses delegation_specifier patterns.
  • Swift, C#, Rust, and PHP each require specialized queries for their unique heritage expressions, trait implementations, generic base lists, and null-safe call expressions.
  • Optional dependencies like the Swift parser are loaded with graceful degradation to prevent ingestion pipeline failures.

Frequently Asked Questions

What are language-specific parsing quirks in tree-sitter grammars?

Language-specific parsing quirks refer to the structural and naming differences between tree-sitter ASTs for different programming languages. For example, while most object-oriented languages have explicit nodes for class declarations, Kotlin represents interfaces as class_declaration nodes with an "interface" keyword literal. GitNexus handles these variations through dedicated query files that map each language's unique node types to a standardized symbol schema.

How does GitNexus distinguish between TypeScript and JavaScript class definitions?

GitNexus maintains separate query constants—TYPESCRIPT_QUERIES and JAVASCRIPT_QUERIES—in tree-sitter-queries.ts to handle their distinct AST nodes. TypeScript uses type_identifier for class and interface names, while JavaScript uses the generic identifier node. Additionally, TypeScript nests inheritance information inside extends_clause nodes, whereas JavaScript places parent identifiers directly inside class_heritage nodes.

Why does Kotlin require special handling for interface declarations?

The tree-sitter Kotlin grammar does not define a dedicated interface_declaration node. Instead, interfaces are represented as class_declaration nodes that contain the literal keyword "interface". GitNexus captures these by matching the keyword pattern within class_declaration nodes and explicitly emitting @definition.interface tags, ensuring interfaces are correctly categorized despite the underlying grammatical ambiguity.

How does GitNexus manage optional language parsers like Swift?

GitNexus loads the Swift parser as an optional dependency inside gitnexus/src/core/tree-sitter/parser-loader.ts using a try/catch block. When the native tree-sitter-swift binding is unavailable, the catch block gracefully skips Swift file processing rather than terminating the ingestion pipeline. This approach allows GitNexus to operate fully even when certain language parsers are not installed, while still supporting Swift analysis when the dependency is present.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →