Преобразование машинописного текста перед проверкой типов

Возьмем машинописный файл:

class A {
    private x? = 0;
    private y? = 0;

    f() {
        console.log(this.x, this.y);
        delete this.x;
    }
}

const a = new A();
a.f();

Я создаю его в веб-пакете, используя awesome-typescript-loader:

{
  test: /\.tsx?$/,
  include: path.resolve("./src"),
  exclude: path.resolve("./node_modules/"),
  use: {
    loader: 'awesome-typescript-loader',
    options: {
      getCustomTransformers: program => ({ 
        before: [deleteTransformer(program)]
      })
    }
  }
},

Где deleteTransformer - мой собственный преобразователь, который заменяет любое выражение delete на delete this.y:

import * as ts from "typescript";

export default function getCustomTransformers(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
  return (context: ts.TransformationContext) => (file: ts.SourceFile) => visitNodeAndChildren(file, program, context);
}

function visitNodeAndChildren<N extends ts.Node>(node: N, program: ts.Program, context: ts.TransformationContext): N {
  return ts.visitEachChild(visitNode(node, program), childNode => visitNodeAndChildren(childNode, program, context), context);
}

function visitNode<N extends ts.Node>(node: N, program: ts.Program): N {
  if (ts.isDeleteExpression(node)) {
    return ts.factory.createDeleteExpression(ts.factory.createPropertyAccessExpression(
      ts.factory.createThis(),
      "y",
    )) as ts.Node as N;
  }

  return node;
}

Если я запущу компиляцию, я получу ожидаемый код (удаляет y, а не x):

/***/ "/7QA":
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var A = /** @class */ (function () {
    function A() {
        this.x = 0;
        this.y = 0;
    }
    A.prototype.f = function () {
        console.log(this.x, this.y);
        delete this.y;
    };
    return A;
}());
var a = new A();
a.f();


/***/ }),

Но если я изменю имя y на z, которого нет в классе A, я не получу сообщения об ошибке.

Также, если я изменю класс A на необязательный x и оставлю y в трансформаторе, я получу сообщение об ошибке

× 「atl」: Checking finished with 1 errors

ERROR in [at-loader] ./src/index.ts:7:16
    TS2790: The operand of a 'delete' operator must be optional.

В соответствии с этими фактами я понимаю, что преобразователь применяется после фактической проверки кода, но преобразователь включен в раздел before, поэтому я ожидаю, что машинописный текст будет проверять сгенерированный код вместо оригинала.

Почему это происходит? В чем разница между трансформаторами before и after в объекте getCustomTransformers (я пробовал оба и не нашел никакой разницы)? И как я могу применить преобразования до того, как код будет проверен?




Ответы (1)


На высоком уровне компилятор TypeScript был разработан для выполнения следующих шагов в следующем порядке:

Parse -> Bind -> Type Check -> Emit (transform)

Из-за такой конструкции код проверки типов часто предполагает, что AST, созданный при синтаксическом анализе, соответствует тексту исходного файла и не изменился.

Например:

// `declaration` is a variable declaration with type `number`
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration) // number
));

declaration = factory.updateVariableDeclaration(
    declaration,
    declaration.name,
    /* exclamation token */ undefined,
    /* type */ factory.createTypeReferenceNode("Date", undefined),
    /* initializer */ undefined,
);

// now type checking won't be reliable
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration) // still number
));
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration.type!) // any
));

Таким образом, вы не можете надежно преобразовать AST, а затем проверить тип, используя существующий код TypeScript Compiler API. Это одна из причин, по которой ts-morph фактически изменяет текст вместо AST, а затем перестраивает АСТ. Для этого необходимо обновить текст исходного файла и множество внутренних свойств. Тем не менее, в определенных сценариях это может сойти с рук ...

Я не уверен, какие усилия потребуются команде TS, чтобы обновить компилятор для обработки преобразований перед проверкой типов, и я не уверен, что они вложили бы в это усилия, но вы можете поговорить с ними и спроси об этом. Просмотрите в checker.ts все вызовы, ведущие to getTextOfNodeFromSourceText для множества случаев, когда это может стать проблемой.

Разница между before и after в getCustomTransformers

Как вы заметили, оба этих преобразования используются при испускании, а не раньше.

  • before - Преобразования, которые необходимо оценить до того, как компилятор выполнит свои преобразования - он по-прежнему будет иметь код TypeScript в AST.
  • after - Преобразования для оценки после того, как компилятор выполнит свои преобразования - он будет преобразован в любую цель (например, печать AST даст код JavaScript).

Дополнительные сведения см. В объявлениях типов.

person David Sherret    schedule 18.09.2020
comment
Это печально ... Постараюсь воспользоваться вашими подсказками. - person Qwertiy; 18.09.2020