Code transformation magic
Valeriy Kuzmin, Lilt, 2021
Previously
- Any JS/TS code can be treated as data
- AST Explorer helps a lot to understand things
- Custom eslint rules are written as abstract syntax tree visitors
- You can actually modify that tree!
The plan and goals
- Give a small reminder about the AST parsing
- Give a simple example of eslint-ignoring
- Give a lot of tips and tricks for how to do it well
- Give a hardcore example of Rust-to-TS (next time)
1 - Parsing reminder
Trick 1: use (@typescript-eslint/parser + transform:jscodeshift)
const Promise = require("Promise");
2 - Move eslint exceptions to the level of files
// eslint-disable-next-line local-rules/no-bluebird
const Promise = require("bluebird");
const Promise = require("bluebird");
2 - A bit of TDD
// eslint-inline-ignores.spec.ts
import { defineTest } from "jscodeshift/dist/testUtils";
jest.autoMockOff();
defineTest(
__dirname,
"eslint-inline-ignores",
{ extensions: "ts" },
"eslint-inline-ignores-no-bluebird",
{
parser: "ts"
}
Trick 2: use jscodeshift defineTest helper
2 - A bit of TDD
// __testfixtures__/eslint-inline-ignores-no-bluebird.input.ts
import Promise1 from "bluebird";
const Promise2 = require("bluebird");
// __testfixtures__/eslint-inline-ignores-no-bluebird.output.ts
// eslint-disable-next-line local-rules/no-bluebird
import Promise1 from "bluebird";
// eslint-disable-next-line local-rules/no-bluebird
const Promise2 = require("bluebird");
2 - Detection
import { API, ASTPath, FileInfo } from "jscodeshift/src/core";
import { namedTypes } from "ast-types";
import ImportDeclaration = namedTypes.ImportDeclaration;
import VariableDeclaration = namedTypes.VariableDeclaration;
const transformation = (file: FileInfo, api: API): string => {
const j = api.jscodeshift;
return j(file.source)
.find(j.ImportDeclaration)
.replaceWith((path: ASTPath<ImportDeclaration>) => {
// TODO
})
.toSource();
};
2 - Replacement - imports
replaceWith((path: ASTPath<ImportDeclaration>) => {
const { value: originalImportDeclaration } = path;
return j.importDeclaration.from({
comments: [
j.commentLine(
` eslint-disable-next-line local-rules/no-bluebird`,
true,
false
)
],
source: originalImportDeclaration.source,
specifiers: originalImportDeclaration.specifiers
});
});
Trick 3: comments cannot be inserted alone
Comments may appear as statements in otherwise empty statement lists, but may not coexist with non-Comment nodes.
2 - Replacement - requires
replaceWith((path: ASTPath<VariableDeclaration>) => {
const { value: originalDeclaration } = path;
// ... a lot of checks ...
return j.variableDeclaration.from({
comments: [
j.commentLine(
` eslint-disable-next-line local-rules/no-bluebird`,
true,
false
)
],
kind: originalDeclaration.kind,
declarations: originalDeclaration.declarations
});
});
2 - Typescript love-hate
const { declarations } = originalDeclaration;
if (declarations.length !== 1) {
return originalDeclaration;
}
const firstDeclaration = declarations[0];
if (firstDeclaration.type !== "VariableDeclarator") {
return originalDeclaration;
}
const { init } = firstDeclaration;
if (!init || init.type !== "CallExpression") {
return originalDeclaration;
}
const { callee, arguments: initArgs } = init;
if (callee.type !== "Identifier" || callee.name !== "require") {
return originalDeclaration;
}
if (initArgs.length !== 1) {
return originalDeclaration;
}
const firstArg = initArgs[0];
if (
!firstArg ||
firstArg.type !== "StringLiteral" ||
firstArg.value !== "bluebird"
) {
return originalDeclaration;
}
2 - Typescript love-hate
✕ transforms correctly using "eslint-inline-ignores-no-bluebird" data (4476 ms)
● eslint-inline-ignores › transforms correctly using "eslint-inline-ignores-no-bluebird" data
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Trick 4: avoid errors at all costs
Trick 5: do not use _.get, use .type checks
2 - Multiple replacements
const afterFirstTransform = j(file.source)
.find(j.ImportDeclaration)
.replaceWith((path: ASTPath<ImportDeclaration>) => {
...
})
.toSource();
const afterSecondTransform = j(file.source)
.find(j.ImportDeclaration)
.replaceWith((path: ASTPath<VariableDeclaration>) => {
...
})
.toSource();
Trick 6: parse and replace separately for different transformations
3 - Tips and tricks recap
- use (@typescript-eslint/parser + transform:jscodeshift)
- use jscodeshift defineTest helper
- comments cannot be inserted alone
- avoid errors at all costs
- do not use _.get, use .type checks
- parse and replace separately for different transformations
Thanks!
questions?
How do we continue?
Full code at https://github.com/lilt/front/pull/9520
jscodeshift-1-at-lilt
By Valeriy Kuzmin
jscodeshift-1-at-lilt
- 249