Inception coding: write code to analyze your code (in Typescript)
Tags: typescript, webdev, markdown
Published: 2020-12-10 09:32:18

If you want to get some statistics about code usage or generate some documentation about it, you are it the right place !

Let's investigate how to analyze your code in this article.

Better than using regex which is a sequence of characters comparison, we  will explain how you can achieve this more efficiently thanks to an AST tree  which uses syntactical analysis.

We will develop this around our own usecase: find all calls to an analytic service and create a markdown report (which will be used by our product team).

*This work is inspired by the work made by nfroidure in [jsarch](https://github.com/nfroidure/jsarch)*


## Step 0: Package installation

Here is all package we will use in this article:
```shell
npm install -D ts-node @babel/parser ast-types glob-promise json2md lodash
```
Our script will be written in typescript and will be launched by a npm command, so we have to add it in the script section of our package.json:
```json
"scripts": {
    "analytics": "ts-node -O '{\"module\":\"commonjs\"}' bin/parseAnalytics.ts"
}
```
*`ts-node` is needed to convert our typescript script, and a little option because we will use `import` statements.*


## Step 1: Grab files
The first step is pretty simple. To optimize and not reading all files in our project, we will generate a list of files to visit.
To do that, we will use [node-glob](https://github.com/isaacs/node-glob#readme).

In our case, we want to visit `javascript` or `typescript` files in our `/src` folder.

```typescript
const filesPaths = await glob("src/**/*.{ts,js}", {
    cwd: process.cwd(),
    dot: true,
    nodir: true,
    absolute: true,
});
```

*`process.cwd()` returns the current directory, since we are launching with npm, it is our project folder.*


## Step 2: File content to AST
To be able to analyze our code, we will generate an AST tree from our code.

Quickly, here is the definition of an AST tree: 
> An abstract syntax tree (AST) is a tree representation of the abstract syntactic structure of source code written in a programming language.

![](https://)![](https://sencrop-public-images.s3.eu-central-1.amazonaws.com/blog/ast.jpg)


So with our files list, we read their content with the `fs` library.
```typescript=
const content = fs.readFileSync(filePath, "utf-8");
```
Then, we create our AST with the help of babel-parser (you can add plugins depending of your needs):
```typescript
const ast = parse(content, {
    sourceType: "module",
    plugins: ["typescript", "jsx", "classProperties"],
});
```



## Step 3: Visit AST

In this step, we visit our AST tree to convert it into a easier structure (in our case it will be an array with all analytic calls, at the moment it just contains the name of the event).
There are a lot of visit functions available (more or less one per AST type), so we first need to find which visit function is the most appropriate.

To do that, copy-paste one example of file you want to visit in an AST tree explorer, for example [ast-explorer](https://astexplorer.net/).

In our case, our call that we want to analyze is a ExpressionStatement: 
```typescript
Analytics.track({
    event: 'This is a event',
    properties: {
        device: 1
    },
});
```

So we will implement the `visitExpressionStatement`: 

```typescript
const analyticsCalls = [];
 visit(ast, {
      visitExpressionStatement: function (path) {
          // Implement your logic here to grab correct parameter
          // In our case, we retrieve the event sended
          if (path.value.expression.callee?.property?.name === 'track') {
            const analyticsParams = path.value.expression.arguments[0];
            const analyticEventName = analyticsParams.properties.find(
              (prop) => prop.key.name === 'event',
            );
            analyticsCalls.push({
              event: analyticEventName?.value?.value || 'N/D',
            });
          }
          this.traverse(path);
      },
 });
```

In the visit function, we explore the properties from the AST node and create an object (with the analytic event name) to push it into an array for the next step.
This is the hardest step, but the AST explorer will help you find the correct visit function and the properties of each AST node. 

*The `traverse` function must be called to continue to explore the AST until the end of it.*


## Step 4: Generate your markdown

Finally, we use the array that we have generated before to create a markdown, by using [json2md package](https://github.com/IonicaBizau/json2md#readme): 

```typescript
json2md([
    { h1: 'Analytics' },
    {
      table: {
        headers: ['Event'],
        rows: analyticsCalls.map((analyticsCall) => ({
          Event: analyticsCall.event,
        })),
      },
    },
]);
```



## Final script 

Here is a recap of the script: 

```typescript
import { parse } from '@babel/parser';
import { visit } from 'ast-types';
import fs from 'fs';
import glob from 'glob-promise';
import json2md from 'json2md';
import { flatten } from 'lodash';

type AnalyticsElement = {
  event: string;
};

async function parseAnalytics() {
  const cwd = process.cwd();

  const filesPaths: string[] = await glob('src/**/*.{ts,js}', {
    cwd,
    dot: true,
    nodir: true,
    absolute: true,
  });

  const analyticsCalls = await Promise.all(
    filesPaths.map((file) => extractAnalytics(file)),
  );

  const markdownContent = generateMarkdown(flatten(analyticsCalls));

  fs.truncateSync('ANALYTICS.md');
  fs.writeFileSync('ANALYTICS.md', markdownContent);
}

async function extractAnalytics(filePath: string): Promise<AnalyticsElement[]> {
  const content = fs.readFileSync(filePath, 'utf-8');

  const ast = parse(content, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx', 'classProperties'],
  });

  const analyticsCalls: AnalyticsElement[] = [];

  visit(ast, {
    visitExpressionStatement: function (path) {
      if (path.value.expression.callee?.property?.name === 'track') {
        const analyticsParams = path.value.expression.arguments[0];
        const analyticEventName = analyticsParams.properties.find(
          (prop) => prop.key.name === 'event',
        );
        analyticsCalls.push({
          event: analyticEventName?.value?.value || 'N/D',
        });
      }
      this.traverse(path);
    },
  });

  return analyticsCalls;
}

function generateMarkdown(analyticsCalls: AnalyticsElement[]): string {
  return json2md([
    { h1: 'Analytics' },
    {
      table: {
        headers: ['Event'],
        rows: analyticsCalls.map((analyticsCall) => ({
          Event: analyticsCall.event,
        })),
      },
    },
  ]);
}

parseAnalytics();

```

## Conclusion

You should now be able to adapt this usecase to suit your needs, and we hope you learnt things with this article!
Thanks for reading!


## Authors

* [Nicolas D.](https://twitter.com/NDelpi)
* [Florian L.](https://www.linkedin.com/in/florian-lauer/)