Gogocode steps on the pit and implements function instrumentation

Gogocode steps on the pit and implements function instrumentation

What is gogocode

Post an official introduction here:

  1. The easiest and most readable AST processing tool on the whole network!
  2. If you need to upgrade, modify, analyze the code, any processing that can be done through AST, you can use GOGOCODE to quickly solve the problem.
  3. No need for traverse, like peeling an onion, layer by layer comparison operations, construction of ast nodes, and even no need to understand what CallExpression, Identifier, and ImportDeclaration are

What is function instrumentation

Instrumentation : Insert or modify some code into some positions in the target program code, so as to obtain and analyze some program states during the running of the target program. Simply put, it is to insert code in the code . Then function instrumentation is to insert or modify the code in the function. In the implementation process, we first introduce, and then run in various types of function bodies.

Today we are going to use gogocode to try to implement instrumentation in a simple situation. The main is to demonstrate gogocode compared babel and jscodeshift some differences, with more features api.

Implementation steps

  1. First prepare a js file as the target, and use gogocode to introduce it.
  2. Traverse the import block to check whether a function package has been imported. If it is not introduced, a code block is generated for introduction.
  3. Determine the function body type to call the instrumentation function, and call the instrumentation function according to the type

Pre-content

  1. All content will use typescript .
  2. There should be an ast foundation. But the introduction of gogocode says that it can be used if you don't know the related types, you can try it.

Prepare the target file and import

target document:

//source.js Import FF from 'AA' ; Import * AS BB from 'BB' ; Import {CC} from 'CC' ; Import 'dd' ; const x = 1 ; function a () { console .log( 'aaa' ); } class B { bb () { Return 'BBB' ; } } const c = () => x; const d = function () { console .log( 'ddd' ); } Copy code

Here is the example of Shenguangda in the babel booklet. The booklet is very conscientious and rich in content. I hope you can support it~

Import files:

//index.ts import $ from "gogocode" $.loadFile( "./source.js" ) Copy code

loadFile
is true
fs.readFileSync
One layer of packaging. Use this method to import files to directly generate gogoAST, which is convenient for our next operations.

Get ast type

Next we begin to face the code AST operation.

Since gogocode itself does not expose the set of available types, we need to use

returnType
Method to get the type of gogoAST:

Import $ from "gogocode" Export type AstType ReturnType = < typeof $> copy the code

Later, we will use the input parameters of this type specification function many times.

Query whether the instrumentation function has been introduced

Here we simulate the instrumentation function as track , and use gogocode to query:

/** * Determine whether the track package has been imported */ export function haveImport ( code: AstType ): boolean { let flag = false code .find( `import $_$1 from "$_$2"` ) .each( item => { if (item.match[ 2 ][ 0 ].value === "track" ) { flag = true } }) return flag } Copy code

Here we used

find
Method, the find method receives a string, and the _ or $$$ placeholder can be used to place the uncertain part in the string . Here I want to check the import part. I'm not sure what is introduced in the code and what is named, so I use _ 1 and _ 2 as placeholders.

find
The method returns an array of results, you can use the method
each
Iterate over the results. Since we used placeholders, the result will have a match attribute, and the content of the placeholder part is under the attribute. The content after _ is the key in the match.

For example, here we need to determine whether the file has introduced a track, so we need to determine whether the content matched by _ 2 is equal to the track.

item.match [ 2 ] [ 0 ] .Value === "Track" copy the code

There is a problem here, the semantics of numbers will affect the readability of the code. Using numbers as keys may not meet your needs, you can also use characters at this time

the Find ( `Import $ _ $ name from" $ _ $ Source "` ) Copy the code

So when we use it, we can use characters as the key

item.match [ "Source" ] [ 0 ] .Value === "Track" copy the code

If you are using TypeScript at this time, you will encounter new problems. The type specifies that the key of the match must be a number. The current alternative is to prefix the ts ignore tag. Wait for the official fix~

In our example code, the track package is not introduced. Next, we will add a reference.

Introduce target files that have not been introduced

The introduction should be before all the content, that is, the top position, we can directly insert in this way:

/** * Introduce track package */ export function insertTrack ( code: AstType ): AstType { return code .before( `import track from "track";/n` ) //Don't forget the cute newline } Copy code

But this does not meet our requirements. Generally, the name of the instrumented function will be unique and easy to retrieve. Here we use a function to generate an incremental uid. Of course, this is not strict, and other methods should be used to generate it in a production environment.

/** * Return uid */ export const generateUid = function () { let uid = 0 function add () { uid++ return uid } function get () { return uid } return { add, get } }() Copy code

Then we use the uid and track to splice:

export function insertTrack ( code: AstType ): AstType { const trackName = `track_ ${generateUid.add()} ` return code .before( `import ${trackName} from "track";/n` ) } Copy code

So the top code we generated is almost like this:

import track_1 from "track" Copy code

The function has been introduced, but before instrumenting, we still need to solve a small problem.

Convert the arrow function to a function with return

There is a function like this in our example:

const C = () => X; duplicated code

We have to add such code inside

track(); copy the code

Then it must first be transformed into this

const c = () => { //To insert code here return x; } Copy code

So we write a conversion function method

/** * The arrow function is converted to a named function */ export function turnArrowToAnonymous ( code: AstType ): AstType { return code .find( `const $_$1 = () => "$_$2"` ) //"" must be added here, otherwise there will be a parsing error. each( node => { if (node.has( `() => $_$` )) { //Make a second judgment and filter out the named function node.replace( `const $_$1 = () => "$_$2"` , `const $_$1 = function() {return "$_$2";}/n` ) } }) .root() } Copy code

Careful friends have discovered that we use a new api in this place,

has
. This api helps us determine whether the function found by find has a part of the identification type.

So the question is, why do you need to make a second judgment?

const $_$1 = () => "$_$2"
Some arrow functions should have been filtered out?

But in fact, this way of screening will also screen this kind of

const d = function () { console .log( 'ddd' ); } Copy code

I'm going to talk about it here

find
Method implementation, although we input a template string, gogocode will not use it directly to do regular matching (of course not). Gogocode will convert these into specific input types and then perform matching. I guess there may be some overlap in this place.

After two screenings, we finally found the arrow function we were looking for. Then we replace, here we use the replace method, use the corresponding template, and change the structure. It's still very convenient here.

But be careful, the format must be correct. Relative to ast illegal code will throw an error during the compilation process.

So, what is "illegal relative to ast"? It's very simple, paste your code to

https://astexplorer.net/
Look at the above to see if the ast tree can be recognized. If it cannot be recognized, it is illegal relative to ast.

Now that our functions are transformed into functions that can be instrumented, let's start instrumenting.

Instrumentation

Before instrumenting, we need to determine the function type. Although functions are all functions in our eyes, they are completely different things in the ast tree.

//FunctionDeclaration function a () { console .log( 'aaa' ); } //FunctionExpression const d = function () { console .log( 'ddd' ); } class B { bb () { Return 'BBB' ; } } Copy code

FunctionDeclaration

This is the most standardized function, and we like it the most.

export function insertFunc ( code: AstType ): AstType { return code .find( `function $_$ (){}` ).each( node => { node.prepend( "body" , `track_ ${generateUid.get()} ();/n` ) }) .root() } Copy code

Here we use prepend, this method requires us to pass in an ast node in the form of an array, and then it will add the code to the top of the array.

FunctionExpression

Compared with the Declaration type, this type has no name, and its naming is outside the function. But the name has little to do with us, we can operate on the part of the function body

export function insertAnonymousFunc ( code: AstType ) { return code .find( ` const $_$1 = $_$body ` ) .each( funcNode => { //@ts-ignore const bodyAst = $(funcNode.match[ "body" ][ 0 ].node).find( `{}` ) //Find the function body bodyAst.prepend( " body" , `track_ ${generateUid.get()} ();/n` ) }) .root() } Copy code

So far we have completed the conversion of the target file.

summary

Generally speaking, it is a well-thought-out tool, encapsulated for many codeMod tools. It is more than enough for simple code conversion requirements. If you need it, you can read the 10-minute document to get started. But there are still some small problems here, and I hope that those who are able to take the time to support them.

  1. The question of ts support. First of all, there is no type declaration out of the box like jscodeshift, which needs to be deduced by the user. And the type handling in match is somewhat problematic.

  2. Use the template string to query, the direction is good. It is very convenient to operate, but another problem has arisen. The writing of strings is not completely free. Friends who have just started may use this way.

    Find ( `_ $ _ $ $ $ 2. 1 = () => {}` ) copy the code

    Because you are not sure how the variable was declared and what it became. So you choose to use two placeholders. But this will report an error. Because it does not conform to ast's intuitive writing. Therefore, those who want to be proficient in using such tools still have to lay a good foundation for ast.