@qt-kaneko/assertype
Version:
TypeScript type guard generator.
404 lines (399 loc) • 13.9 kB
JavaScript
// src/main.ts
import ts3 from "typescript";
import path from "path";
import fs from "fs";
// src/factory.ts
import ts from "typescript";
function createTypePredicateNode({
assertsModifier,
parameterName,
type
}) {
return ts.factory.createTypePredicateNode(
assertsModifier,
parameterName,
type
);
}
function createFunctionDeclaration({
modifiers,
asteriskToken,
name,
typeParameters,
parameters,
type,
body
}) {
return ts.factory.createFunctionDeclaration(
modifiers,
asteriskToken,
name,
typeParameters,
parameters,
type,
body
);
}
function createArrowFunction({
modifiers,
typeParameters,
parameters,
type,
equalsGreaterThanToken,
body
}) {
return ts.factory.createArrowFunction(
modifiers,
typeParameters,
parameters,
type,
equalsGreaterThanToken,
body
);
}
function createCallExpression({
expression,
typeArguments,
argumentsArray
}) {
return ts.factory.createCallExpression(
expression,
typeArguments,
argumentsArray
);
}
function createParameterDeclaration({
modifiers,
dotDotDotToken,
name,
questionToken,
type,
initializer
}) {
return ts.factory.createParameterDeclaration(
modifiers,
dotDotDotToken,
name,
questionToken,
type,
initializer
);
}
function createBindingElement({
dotDotDotToken,
propertyName,
name,
initializer
}) {
return ts.factory.createBindingElement(
dotDotDotToken,
propertyName,
name,
initializer
);
}
// src/createCheck.ts
import ts2 from "typescript";
var ELEMENT = `e`;
var KEY = `k`;
var VALUE = `v`;
function* createCheck(value, type, typeChecker2, printer2) {
let typeofMap = {
[ts2.TypeFlags.String]: `string`,
[ts2.TypeFlags.Number]: `number`,
[ts2.TypeFlags.BigInt]: `bigint`,
[ts2.TypeFlags.Undefined]: `undefined`
};
if (type.flags in typeofMap) {
return yield ts2.factory.createStrictEquality(
ts2.factory.createTypeOfExpression(value),
ts2.factory.createStringLiteral(typeofMap[type.flags])
);
}
if (type.isUnion() && type.types.every((type2) => type2.flags === ts2.TypeFlags.BooleanLiteral)) {
return yield ts2.factory.createStrictEquality(
ts2.factory.createTypeOfExpression(value),
ts2.factory.createStringLiteral(`boolean`)
);
}
if (typeChecker2.isArrayType(type)) {
return yield* createArrayCheck(value, type, typeChecker2, printer2);
}
if (type.isClass()) {
return yield ts2.factory.createBinaryExpression(
value,
ts2.SyntaxKind.InstanceOfKeyword,
ts2.factory.createIdentifier(type.symbol.name)
);
}
if (type.flags === ts2.TypeFlags.Object) {
return yield* createObjectCheck(value, type, typeChecker2, printer2);
}
if (type.isUnion()) {
return yield* createUnionCheck(value, type, typeChecker2, printer2);
}
if (type.isIntersection()) {
return yield* createIntersectionCheck(value, type, typeChecker2, printer2);
}
if (type.flags === ts2.TypeFlags.Null) {
return yield ts2.factory.createStrictEquality(
value,
ts2.factory.createNull()
);
}
if (type.isStringLiteral()) {
return yield ts2.factory.createStrictEquality(
value,
ts2.factory.createStringLiteral(type.value)
);
}
if (type.isNumberLiteral()) {
return yield ts2.factory.createStrictEquality(
value,
ts2.factory.createNumericLiteral(type.value)
);
}
if (type.flags === ts2.TypeFlags.TemplateLiteral) {
return yield* createTemplateLiteralCheck(value, type, typeChecker2, printer2);
}
if (type.flags === ts2.TypeFlags.BooleanLiteral) {
return yield ts2.factory.createStrictEquality(
value,
type === typeChecker2.getTrueType() ? ts2.factory.createTrue() : ts2.factory.createFalse()
);
}
if (type.flags === ts2.TypeFlags.Unknown) {
return;
}
throw new Error(`Type '${typeChecker2.typeToString(type)}' on '${printer2.printNode(ts2.EmitHint.Expression, value, value.getSourceFile())}' with flags '${ts2.TypeFlags[type.flags]}' is not supported.`);
}
function* createArrayCheck(value, type, typeChecker2, printer2) {
yield createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(
ts2.factory.createIdentifier(`Array`),
`isArray`
),
argumentsArray: [value]
});
let elementType = typeChecker2.getTypeArguments(type)[0];
let identifier = ts2.factory.createIdentifier(ELEMENT);
let parameter = createParameterDeclaration({ name: identifier });
let checks = createCheck(identifier, elementType, typeChecker2, printer2).toArray();
if (checks.length === 0) return;
let check = checks.reduce(ts2.factory.createLogicalAnd);
let lambda = createArrowFunction({
parameters: [parameter],
body: check
});
let every = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(value, `every`),
argumentsArray: [lambda]
});
yield every;
}
function* createObjectCheck(value, type, typeChecker2, printer2) {
yield ts2.factory.createStrictEquality(
ts2.factory.createTypeOfExpression(value),
ts2.factory.createStringLiteral(`object`)
);
yield ts2.factory.createStrictInequality(
value,
ts2.factory.createNull()
);
yield* createObjectIndexCheck(value, type, typeChecker2, printer2);
let properties = typeChecker2.getPropertiesOfType(type);
for (let property of properties) {
let propertyType = typeChecker2.getTypeOfSymbol(property);
let indexer = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property.name) ? ts2.factory.createPropertyAccessExpression(value, property.name) : ts2.factory.createElementAccessExpression(value, ts2.factory.createStringLiteral(property.name));
let check = createCheck(indexer, propertyType, typeChecker2, printer2).toArray();
if (check.length === 0) continue;
yield check.reduce(ts2.factory.createLogicalAnd);
}
}
function* createObjectIndexCheck(value, type, typeChecker2, printer2) {
let indexes = typeChecker2.getIndexInfosOfType(type);
for (let index of indexes) {
let keyType = index.keyType;
let valueType = index.type;
if (keyType.flags === ts2.TypeFlags.String) {
yield* createObjectStringIndexCheck(value, valueType, typeChecker2, printer2);
} else if (keyType.flags === ts2.TypeFlags.TemplateLiteral) {
yield* createObjectUniversalIndexCheck(value, keyType, valueType, typeChecker2, printer2);
} else {
throw new Error(`Key type '${typeChecker2.typeToString(keyType)}' on '${printer2.printNode(ts2.EmitHint.Expression, value, value.getSourceFile())}' with flags '${ts2.TypeFlags[keyType.flags]}' is not supported.`);
}
}
}
function* createObjectStringIndexCheck(value, valueType, typeChecker2, printer2) {
let values = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(
ts2.factory.createIdentifier(`Object`),
`values`
),
argumentsArray: [value]
});
let identifier = ts2.factory.createIdentifier(VALUE);
let parameter = createParameterDeclaration({ name: identifier });
let checks = createCheck(identifier, valueType, typeChecker2, printer2).toArray();
if (checks.length === 0) return;
let check = checks.reduce(ts2.factory.createLogicalAnd);
let lambda = createArrowFunction({
parameters: [parameter],
body: check
});
let every = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(values, `every`),
argumentsArray: [lambda]
});
yield every;
}
function* createObjectUniversalIndexCheck(value, keyType, valueType, typeChecker2, printer2) {
let entries = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(
ts2.factory.createIdentifier(`Object`),
`entries`
),
argumentsArray: [value]
});
let keyIdentifier = ts2.factory.createIdentifier(KEY);
let valueIdentifier = ts2.factory.createIdentifier(VALUE);
let keyValueBinding = ts2.factory.createArrayBindingPattern([
createBindingElement({ name: keyIdentifier }),
createBindingElement({ name: valueIdentifier })
]);
let parameter = createParameterDeclaration({ name: keyValueBinding });
let keyChecks = createCheck(keyIdentifier, keyType, typeChecker2, printer2).toArray();
if (keyChecks.length === 0) return;
let keyCheck = keyChecks.reduce(ts2.factory.createLogicalAnd);
let valueChecks = createCheck(valueIdentifier, valueType, typeChecker2, printer2).toArray();
if (valueChecks.length === 0) return;
let valueCheck = valueChecks.reduce(ts2.factory.createLogicalAnd);
let check = ts2.factory.createLogicalAnd(keyCheck, valueCheck);
let lambda = createArrowFunction({
parameters: [parameter],
body: check
});
let every = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(entries, `every`),
argumentsArray: [lambda]
});
yield every;
}
function* createUnionCheck(value, type, typeChecker2, printer2) {
let types = type.types;
let checks = types.map((type2) => createCheck(value, type2, typeChecker2, printer2)).map((checks2) => checks2.toArray()).filter((checks2) => checks2.length > 0).map((checks2) => checks2.reduce(ts2.factory.createLogicalAnd));
if (checks.length === 0) return;
let check = checks.reduce(ts2.factory.createLogicalOr);
yield check;
}
function* createIntersectionCheck(value, type, typeChecker2, printer2) {
let types = type.types;
let checks = types.map((type2) => createCheck(value, type2, typeChecker2, printer2)).map((checks2) => checks2.toArray()).filter((checks2) => checks2.length > 0).map((checks2) => checks2.reduce(ts2.factory.createLogicalAnd));
if (checks.length === 0) return;
let check = checks.reduce(ts2.factory.createLogicalAnd);
yield check;
}
function* createTemplateLiteralCheck(value, type, typeChecker2, printer2) {
let texts = type.texts;
let types = type.types;
let regexpText = `/^`;
for (let textI = 0; textI < texts.length; ++textI) {
let text = texts[textI];
text = text.replace(/[$^\\.*+?()[\]{}|/]/g, `\\$&`);
regexpText += text;
if (textI < types.length) {
let type2 = types[textI];
regexpText += typeToRegExp(type2, typeChecker2);
}
}
regexpText += `$/`;
let regexp = ts2.factory.createRegularExpressionLiteral(regexpText);
let test = createCallExpression({
expression: ts2.factory.createPropertyAccessExpression(regexp, `test`),
argumentsArray: [value]
});
yield test;
}
function typeToRegExp(type, typeChecker2) {
if (type.flags === ts2.TypeFlags.BigInt) {
return `-?\\d+`;
}
if (type.flags === ts2.TypeFlags.String) {
return `.*`;
}
throw new Error(`Type '${typeChecker2.typeToString(type)}' in template literal, with flags '${ts2.TypeFlags[type.flags]}' is not supported.`);
}
// src/main.ts
var VALUE2 = `v`;
var args = process.argv.slice(2);
var configPath = args[0];
var configDir = path.dirname(configPath);
var configFile = ts3.readJsonConfigFile(args[0], ts3.sys.readFile);
var config = ts3.parseJsonSourceFileConfigFileContent(configFile, ts3.sys, configDir);
var printer = ts3.createPrinter(config.options);
var program = ts3.createProgram({
rootNames: config.fileNames,
options: config.options,
configFileParsingDiagnostics: config.errors
});
var typeChecker = program.getTypeChecker();
var sources = config.fileNames.map(program.getSourceFile).filter((source) => source !== void 0);
for (let source of sources) {
let text = source.text;
let edits = {};
let statements = source.statements;
let declarations = statements.filter((statement) => ts3.isTypeAliasDeclaration(statement) || ts3.isInterfaceDeclaration(statement));
for (let declaration of declarations) {
let symbol = typeChecker.getSymbolAtLocation(declaration.name);
let marker = `assertype`;
let guardName = symbol.name;
let guard = statements.find((statement) => ts3.isFunctionDeclaration(statement) && statement.name?.text === guardName && ts3.getJSDocTags(statement).some((tag) => tag.tagName.text === marker));
if (guard === void 0) continue;
let hasExport = declaration.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.ExportKeyword) ?? false;
let modifiers = [];
if (hasExport) {
modifiers.push(ts3.factory.createModifier(ts3.SyntaxKind.ExportKeyword));
}
let identifier = ts3.factory.createIdentifier(VALUE2);
let parameter = createParameterDeclaration({ name: identifier });
let type = typeChecker.getDeclaredTypeOfSymbol(symbol);
let guardType = createTypePredicateNode({
parameterName: identifier,
type: typeChecker.typeToTypeNode(type, void 0, void 0)
});
let checks = createCheck(identifier, type, typeChecker, printer).toArray();
if (checks.length === 0) continue;
let check = checks.reduce(ts3.factory.createLogicalAnd);
let $return = ts3.factory.createReturnStatement(check);
let body = ts3.factory.createBlock([$return]);
let pos = guard.pos;
let end = guard.end;
for (let editString in edits) {
let edit = +editString;
if (edit >= pos) break;
let offset2 = edits[edit];
pos += offset2;
end += offset2;
}
let skipWhitespaceRegExp = /[^\s]/g;
skipWhitespaceRegExp.lastIndex = pos;
pos = skipWhitespaceRegExp.exec(text).index;
guard = createFunctionDeclaration({
modifiers,
name: guardName,
parameters: [parameter],
body,
type: guardType
});
let guardText = printer.printNode(ts3.EmitHint.Unspecified, guard, source);
guardText = `/** @ts-ignore @${marker} */ // eslint-disable-next-line
` + guardText;
let length = end - pos;
let offset = guardText.length - length;
edits[pos] = offset;
text = text.slice(0, pos) + guardText + text.slice(end);
}
fs.writeFileSync(source.fileName, text);
}
//# sourceMappingURL=main.js.map