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
Userasdefinition.classdespite thetype_identifiernode type - Kotlin: Captures
Serviceasdefinition.interfacedespite being aclass_declarationnode with"interface"keyword - Swift: Maps
Drawableprotocol todefinition.interfacevia the inheritance_specifier handling - PHP: Captures null-safe call
saveas@callvianullsafe_member_call_expressionquery
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_identifiernodes and nestedextends_clausestructures compared to JavaScript'sidentifierand directclass_heritage. - Kotlin interfaces are captured via
class_declarationnodes with keyword matching rather than dedicated interface nodes, while inheritance usesdelegation_specifierpatterns. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →