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

  1. Give a small reminder about the AST parsing
  2. Give a simple example of eslint-ignoring
  3. Give a lot of tips and tricks for how to do it well
  4. 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

  1. use (@typescript-eslint/parser + transform:jscodeshift)
  2. use jscodeshift defineTest helper
  3. comments cannot be inserted alone
  4. avoid errors at all costs
  5. do not use _.get, use .type checks
  6. parse and replace separately for different transformations

Thanks!

questions?

How do we continue?

jscodeshift-1-at-lilt

By Valeriy Kuzmin

jscodeshift-1-at-lilt

  • 253