DEV Community

akamabe
akamabe

Posted on

Building a TypeScript Code Radar: What an AST Reveals That Text Search Misses

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

as a sequence of characters, Tree-sitter sees something structurally closer to:

decorator
  call_expression
    identifier = "Controller"
    arguments
      string = "'orders'"
Enter fullscreen mode Exit fullscreen mode

And this:

export class OrdersController
Enter fullscreen mode Exit fullscreen mode

becomes a structure containing:

class_declaration
  type_identifier = "OrdersController"
  class_body
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and then ask Tree-sitter for its name field.

My first structural extraction ended up producing:

{ type: 'class_declaration', name: 'OrdersController' }
Enter fullscreen mode Exit fullscreen mode

I then inspected the surrounding decorator nodes and extracted:

{ name: 'Controller', arguments: [ "'orders'" ] }
{ name: 'UseGuards', arguments: [ 'AuthGuard' ] }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)