Every day when working on the code, on the way to implementing useful functionality for the user, there are forced (inevitable, or simply desirable) changes to the code. This may be refactoring, updating a library or framework to a new major version, updating JavaScript syntax (which is not uncommon recently). Even if the library is part of a working draft - changes are inevitable. Most of these changes are routine. There is nothing interesting for the developer on the one hand, on the other it does not bring anything to the business, and on the third, in the update process, you need to be very careful not to break the wood and break the functionality. Thus, we come to the conclusion that it is better to shift such a routine onto the shoulders of the programs, what would they all do themselves, and the person, in turn, would control whether everything was properly done. That is what this article is about.
For programmatic processing of the code, it is necessary to translate it into a special presentation with which it would be convenient for the programs to work. Such a representation exists, it is called Abstract Syntax Tree (AST).
In order to get it, use parsers. The resulting AST can be transformed as you like, and then to save the result you need a code generator. Let us consider in more detail each of the steps. Let's start with the parser.
And so we have the code:
a + b
Usually parsers are divided into two parts:
Splits the code into tokens, each of which describes a part of the code:
[{ "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "+", }, { "type": "Identifier", "value": "b" }]
Builds a syntax tree of tokens:
{ "type": "BinaryExpression", "left": { "type": "Identifier", "name": "a" }, "operator": "+", "right": { "type": "Identifier", "name": "b" } }
And here we already have the very idea with which you can work programmatically. It should be clarified that there are a large number of JavaScript
parsers, here are some of them:
babel
;eslint
;There is a standard JavaScript parsers, it is called ESTree and determines which nodes should parse as it should.
For a more detailed analysis of the implementation process of the parser (as well as the transformer and generator), you can read super-tiny-compiler .
In order to convert an AST tree, you can use the Visitor pattern, for example, using the @ babel / traverse library. The following code will output the names of all the JavaScript code identifiers from the code
variable.
import * as parser from "@babel/parser"; import traverse from "@babel/traverse"; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { Identifier(path) { console.log(path.node.name); } });
You can generate code, for example, using @ babel / generator , thus:
import {parse} from '@babel/parser'; import generate from '@babel/generator'; const code = 'class Example {}'; const ast = parse(code); const output = generate(ast, code);
And so, at this stage, the reader had to get a basic idea of what is needed to transform JavaScript code, and with what tools this is implemented.
It is necessary to add such an online tool as astexplorer , it combines a large number of parsers, transformers and generators.
Putout is a code transformer with plug-in support. In fact, it is a cross between eslint and babel , combining the advantages of both tools.
As eslint
putout
shows problem areas in the code, but unlike eslint
putout
changes the behavior of the code, that is, it is able to correct all errors that it can find.
Like babel
putout
transforms the code, but tries to change it as little as possible, so it can be used to work with code that is stored in the repository.
Another thing worth mentioning is the prettier , it is a formatting tool, and it differs radically.
Jscodeshift is not very far from putout
, but it does not support plugins, does not show error messages, and also uses ast-types instead of @ babel / types .
In the process of work, eslint
helps me a lot with my hints. But sometimes you want more from him. For example, what would he remove the debugger , fix test.only , and also delete unused variables. The last point formed the basis of putout
, in the development process, it became clear that this is not very simple and many other transformations are much easier to implement. Thus, putout
smoothly evolved from one function to a plugin system. Removing unused variables is still the most difficult process, but it doesn’t interfere with the development and maintenance of many other equally useful transformations.
Work putout
can be divided into two parts: the engine and plugins. This architecture allows you to not be distracted by the transformation when working with the engine, and when working on plug-ins you will focus on their purpose.
The work putout
is built on the plugin system. Each plugin represents one rule. Using the built-in rules, you can do the following:
Find and remove:
debugger
test.only
test.skip
console.log
process.exit
Find and split variable declarations:
// было var one, two; // станет var one; var two;
Convert esm
to commonjs
:
// было import one from 'one'; // станет const one = require('one');
// было const name = user.name; // станет const {name} = user;
// было const {name} = user; const {password} = user; // станет const { name, password } = user;
Each plug-in is built according to the Unix Philosophy , that is, they are as simple as possible, each performs one action, making them easy to combine, because they, in essence, are filters.
For example, having the following code:
const name = user.name; const password = user.password;
It is first converted into the following using apply-destructuring :
const {name} = user; const {password} = user;
After that, using merge-destructuring-properties is converted to:
const { name, password } = user;
Thus, plug-ins can work both separately and together. When creating your own plugins, it is recommended to adhere to this rule, and implement a plug-in with minimal functionality that does only what you need, and the plug-in and user plug-ins take care of the rest.
After we have familiarized ourselves with the built-in rules, we can consider an example of using putout
.
Create an example.js
file with the following contents:
const x = 1, y = 2; const name = user.name; const password = user.password; console.log(name, password);
Now, run putout
, passing example.js
as an argument:
coderaiser@cloudcmd:~/example$ putout example.js /home/coderaiser/example/example.js 1:6 error "x" is defined but never used remove-unused-variables 1:13 error "y" is defined but never used remove-unused-variables 6:0 error Unexpected "console" call remove-console 1:0 error variables should be declared separately split-variable-declarations 3:6 error Object destructuring should be used apply-destructuring 4:6 error Object destructuring should be used apply-destructuring 6 errors in 1 files fixable with the `--fix` option
We will receive information containing 6 errors, discussed in more detail above, now we will correct them, and see what happened:
coderaiser@cloudcmd:~/example$ putout example.js --fix coderaiser@cloudcmd:~/example$ cat example.js const { name, password } = user;
As a result of the correction, unused variables and console.log
calls were removed, and destructuring was also applied.
The default settings may not always be suitable for everyone, therefore putout
supports the configuration file .putout.json
, it consists of the following sections:
The rules
section contains a rule system. The rules, by default, are set as follows:
{ "rules": { "remove-unused-variables": true, "remove-debugger": true, "remove-only": true, "remove-skip": true, "remove-process-exit": false, "remove-console": true, "split-variable-declarations": true, "remove-empty": true, "remove-empty-pattern": true, "convert-esm-to-commonjs": false, "apply-destructuring": true, "merge-destructuring-properties": true } }
In order to enable remove-process-exit
enough to set it to true
in the .putout.json
file:
{ "rules": { "remove-process-exit": true } }
This will be enough to report all the process.exit
calls found in the code, and remove them if the --fix
parameter is --fix
.
If you need to add some folders to the list of exceptions, just add the ignore
section:
{ "ignore": [ "test/fixture" ] }
In case of need of an extensive system of rules, for example, enable process.exit
for the bin
directory, just use the match
section:
{ "match": { "bin": { "remove-process-exit": true, } } }
In the case of using plugins that are not embedded and have the putout-plugin-
, they must be included in the plugins
section before being activated in the rules
section. For example, to connect the putout-plugin-add-hello-world
and enable the add-hello-world
rule, it’s enough to specify:
{ "rules": { "add-hello-world": true }, "plugins": [ "add-hello-world" ] }
The putout
engine is a command line tool that reads the settings, parses the files, loads and launches the plugins, and then writes the result of the plugins.
It uses the recast library, which helps to accomplish a very important task: after parsing and transformation, collect the code in a state as close as possible to the previous one.
For parsing, ESTree
compatible parser (currently babel
with an estree
plugin, but changes are possible in the future), and for transformation, babel
tools. Why exactly babel
? It's simple. The fact is that this is a very popular product, much more popular than other similar tools, and it develops much more rapidly. Each new proposal in the standard EcmaScript can not do without a babel-plugin . Babel also has a Babel Handbook book in which all the features and tools for circumventing and transforming an AST tree are described very well.
The putout
plugin system is quite simple, and very similar to the eslint plugins , as well as the babel plugins . True, instead of a single function, the putout
plugin should export 3. This is done to increase the reuse of the code, because duplicating the functionality in 3 functions is not very convenient, it is much easier to put it into separate functions and just call it in the right places.
So Putout
plugin consists of 3 functions:
report
- returns the message;find
- looks for places with errors and returns them;fix
- fix these places;The main point to remember when creating a plugin for putout
is its name, it must begin with putout-plugin-
. Next can be the name of the operation that the plugin performs, for example, the plug-in remove-wrong
should be called like this: putout-plugin-remove-wrong
.
You should also add the words: putout
and putout-plugin
in package.json
, in the keywords
section, and specify "putout": ">=3.10"
in peerDependencies
"putout": ">=3.10"
, or the version that will be the last one at the time of writing the plugin.
Let's write, for example, a plugin that will remove the word debugger
from the code. Such a plugin is already there, it is @ putout / plugin-remove-debugger and it is simple enough to consider it now.
It looks like this:
// возвращаем ошибку соответствующую каждому из найденых узлов module.exports.report = () => 'Unexpected "debugger" statement'; // в этой функции ищем узлы, содержащией debugger с помощью паттерна Visitor module.exports.find = (ast, {traverse}) => { const places = []; traverse(ast, { DebuggerStatement(path) { places.push(path); } }); return places; }; // удаляем код, найденный в предыдущем шаге module.exports.fix = (path) => { path.remove(); };
If a remove-debugger
rule is included in .putout.json
, the @putout/plugin-remove-debugger
will be loaded. First, the find
function is called which, using the traverse
function, traverse
nodes of the AST tree and saves all the necessary places.
The next step putout
will turn to report
for the desired message.
If the --fix
flag is used, the plug-in fix
function will be called and the transformation will be performed, in this case, the node is deleted.
In order to simplify the testing of plugins, the @ putout / test tool was written. At its core, this is nothing more than a wrapper over a tape , with several methods for convenience and ease of testing.
The test for the remove-debugger
plugin can look like this:
const removeDebugger = require('..'); const test = require('@putout/test')(__dirname, { 'remove-debugger': removeDebugger, }); // проверяем что бы сообщение было именно таким test('remove debugger: report', (t) => { t.reportCode('debugger', 'Unexpected "debugger" statement'); t.end(); }); // проверяем результат трансформации test('remove debugger: transformCode', (t) => { t.transformCode('debugger', ''); t.end(); });
Not any transformation needs to be used every day, for one-time transformations, it is enough to do everything the same, only instead of publishing to npm
place it in the ~/.putout
. When launching, putout
will look in this folder, pick up and start transformations.
Here is an example of a transformation that replaces the tape
connection and try-to-tape connection with a supertape call: convert-tape-to-supertape .
Finally, it’s worth adding one thing: putout
tries to change the code minimally, but if it happens to a friend that some formatting rules break, eslint --fix is always ready to eslint --fix
, and for this purpose there is a special plugin eslint-plugin-putout . It can brighten up many formatting errors, and of course it can be configured in accordance with the preferences of developers on a particular project. Connect it easily:
{ "extends": [ "plugin:putout/recommended", ], "plugins": [ "putout" ] }
So far there is only one rule in it: one-line-destructuring
, it does the following:
// было const { one } = hello; // станет const {one} = hello;
There are still many included eslint
rules that you can read in more detail .
I want to thank the reader for paying attention to this text. I sincerely hope that the topic of AST transformations will become more popular, and articles about this fascinating process will appear more often. I would be very grateful for any comments and suggestions related to the future direction of putout
development. Create an issue , send a pool of requests , test, write what rules you would like to see, and how to transform your code programmatically, we will work together to improve the AST transformation tool.
Source: https://habr.com/ru/post/439564/