UNPKG

@qt-kaneko/assertype

Version:

TypeScript type guard generator.

404 lines (399 loc) 13.9 kB
#!/usr/bin/env node // 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