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
Step 0: Package installation
Here is all package we will use in this article:
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:
"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.
In our case, we want to visit javascript
or typescript
files in our /src
folder.
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.
So with our files list, we read their content with the fs
library.
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):
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.
In our case, our call that we want to analyze is a ExpressionStatement:
Analytics.track({
event: 'This is a event',
properties: {
device: 1
},
});
So we will implement the visitExpressionStatement
:
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:
json2md([
{ h1: 'Analytics' },
{
table: {
headers: ['Event'],
rows: analyticsCalls.map((analyticsCall) => ({
Event: analyticsCall.event,
})),
},
},
]);
Final script
Here is a recap of the script:
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!
Top comments (0)