Today I started experimenting with Tree-sitter.
I had heard the name before, but I had never actually used it. My goal was simple: take a TypeScript file, parse it, and understand what Tree-sitter sees that a normal text search does not.
The file I used was a tiny NestJS controller:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { OrdersService } from './orders.service';
@Controller('orders')
@UseGuards(AuthGuard)
export class OrdersController {
constructor(private readonly service: OrdersService) {}
@Get(':id')
findOne() {
return this.service.findOne();
}
}
At first, I printed Tree-sitter's syntax tree directly.
The result was technically correct, but almost unreadable: one huge nested expression containing nodes such as import_statement, class_declaration, decorator, call_expression, and identifier.
So I wrote a small recursive tree printer.
That was the moment Tree-sitter started to make sense.
Instead of seeing:
@Controller('orders')
as a sequence of characters, Tree-sitter sees something structurally closer to:
decorator
call_expression
identifier = "Controller"
arguments
string = "'orders'"
And this:
export class OrdersController
becomes a structure containing:
class_declaration
type_identifier = "OrdersController"
class_body
That distinction is much more important than it initially looks.
Text search asks: "Does this string exist?"
A regular text search can easily find:
@Controller
But it doesn't inherently know whether the string is:
- a decorator,
- a comment,
- part of a string,
- an identifier,
- or something unrelated.
An AST gives that text context.
Instead of searching for the characters OrdersController, I can search for a node whose type is:
class_declaration
and then ask Tree-sitter for its name field.
My first structural extraction ended up producing:
{ type: 'class_declaration', name: 'OrdersController' }
I then inspected the surrounding decorator nodes and extracted:
{ name: 'Controller', arguments: [ "'orders'" ] }
{ name: 'UseGuards', arguments: [ 'AuthGuard' ] }
Now we're no longer dealing with source code as text.
We're turning code into facts.
Conceptually, the pipeline is becoming:
TypeScript source
↓
Tree-sitter
↓
Syntax tree
↓
Structural extraction
↓
Facts about the code
That opens up much more interesting possibilities.
For example, an analyzer could eventually answer questions like:
- Which classes are NestJS controllers?
- Which routes use guards?
- Which controllers are missing authorization guards?
- Which services does a controller depend on?
- How are modules connected?
One important limitation
Tree-sitter understands syntax, not application semantics.
If I write:
@SomethingThatDoesNotExist()
Tree-sitter can still correctly recognize it as a decorator and function call.
It does not know whether that decorator exists in NestJS or whether my application will actually compile.
That separation was useful to understand:
Parsing tells me what the code structurally is.
Other layers — the TypeScript compiler, framework knowledge, static analysis rules — determine what that structure actually means.
This was only a small experiment, but I now understand why Tree-sitter is used as a foundation for editors, code navigation tools, and static analyzers.
Tomorrow, I'll move from inspecting the AST manually to extracting imports, classes, and decorators into normalized facts.
That should be the first real step toward turning this experiment into a small TypeScript code radar.
Top comments (0)