diff --git a/package.json b/package.json index 9ecdc8f4e0df..ec76c0252f77 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@types/node": "^15.12.5", "@types/semver": "^7.3.9", "@types/source-map-support": "^0", + "@types/ts-nameof": "^4.2.1", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "@zwave-js/config": "workspace:*", @@ -75,6 +76,8 @@ "reflect-metadata": "^0.1.13", "semver": "^7.3.5", "source-map-support": "^0.5.21", + "ts-nameof": "^5.0.0", + "ts-patch": "^2.0.1", "typescript": "4.6.2", "zwave-js": "workspace:*" }, @@ -103,7 +106,8 @@ "commit": "git-cz", "release": "release-script", "release:all": "release-script --publish-all", - "postinstall": "husky install", + "postinstall": "husky install ; ts-patch install -s", + "prepare": "ts-patch install -s", "config": "yarn ts packages/config/maintenance/importConfig.ts", "docs": "docsify serve docs", "docs:generate": "yarn ts packages/maintenance/src/generateTypedDocs.ts", diff --git a/packages/maintenance/.gitignore b/packages/maintenance/.gitignore deleted file mode 100644 index 413947f0606a..000000000000 --- a/packages/maintenance/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# AUTO-GENERATED: secondary module exports - -# Put other ignored files here diff --git a/packages/maintenance/tsconfig.build.json b/packages/maintenance/tsconfig.build.json index 0b508dffa5e2..a909f28631df 100644 --- a/packages/maintenance/tsconfig.build.json +++ b/packages/maintenance/tsconfig.build.json @@ -8,6 +8,9 @@ "references": [ { "path": "../core/tsconfig.build.json" + }, + { + "path": "../shared/tsconfig.build.json" } ], "include": ["src/**/*.ts"], diff --git a/packages/maintenance/tsconfig.json b/packages/maintenance/tsconfig.json index aeaa5b6ab31e..464cb561588c 100644 --- a/packages/maintenance/tsconfig.json +++ b/packages/maintenance/tsconfig.json @@ -8,6 +8,9 @@ "references": [ { "path": "../core/tsconfig.build.json" + }, + { + "path": "../shared/tsconfig.build.json" } ], "include": ["src/**/*.ts"], diff --git a/packages/transformers/package.json b/packages/transformers/package.json new file mode 100644 index 000000000000..d2fdb5e1ef9e --- /dev/null +++ b/packages/transformers/package.json @@ -0,0 +1,41 @@ +{ + "name": "@zwave-js/transformers", + "version": "9.0.0-beta.7", + "description": "zwave-js: compile-time transformers", + "private": true, + "keywords": [], + "main": "build/index.js", + "types": "build/index.d.ts", + "files": [ + "build/**/*.{js,d.ts,map}" + ], + "author": { + "name": "AlCalzone", + "email": "d.griesel@gmx.net" + }, + "license": "MIT", + "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + }, + "bugs": { + "url": "https://github.com/AlCalzone/node-zwave-js/issues" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + }, + "engines": { + "node": ">=12.22.2 <13 || >=14.13.0 <15 || >= 16 <16.9.0 || >16.9.0" + }, + "scripts": { + "build": "tsc -b tsconfig.build.json --verbose", + "clean": "yarn run build --clean", + "watch": "yarn run build --watch --pretty", + "test": "yarn run build && tsc -p tsconfig.test.json; node test/test1" + }, + "devDependencies": { + "tsutils": "^3.21.0", + "typescript": "4.6.2" + } +} diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts new file mode 100644 index 000000000000..8db953b1547b --- /dev/null +++ b/packages/transformers/src/index.ts @@ -0,0 +1,62 @@ +// function checkGetErrorObject( +// getErrorObject: unknown, +// ): asserts getErrorObject is (obj: any) => any { +// if (typeof getErrorObject !== "function") { +// throw new Error( +// "This module should not be used in runtime. Instead, use a transformer during compilation.", +// ); +// } +// } + +// /** +// * Checks if the given argument is assignable to the given type-argument. +// * +// * @param object object whose type needs to be checked. +// * @returns `true` if `object` is assignable to `T`, false otherwise. +// * @example +// ``` +// is(42); // -> true +// is('foo'); // -> false +// ``` +// */ +// export function is(object: any): object is T; +// export function is(obj: any, getErrorObject?: (obj: any) => any): obj is T { +// checkGetErrorObject(getErrorObject); +// const errorObject = getErrorObject(obj); +// return errorObject === null; +// } + +// /** +// * Creates a function similar to `is` that can be invoked at a later point. +// * +// * This is useful, for example, if you want to re-use the function multiple times. +// * +// * @example +// ``` +// const checkNumber = createIs(); +// checkNumber(42); // -> true +// checkNumber('foo'); // -> false +// ``` +// */ +// export function createIs(): (object: any) => object is T; +// export function createIs( +// getErrorObject = undefined, +// ): (object: any) => object is T { +// checkGetErrorObject(getErrorObject); +// // @ts-expect-error We're using an internal signature +// return (obj) => is(obj, getErrorObject); +// } + +/** Generates code at build time which validates all arguments of this method */ +export function validateArgs(): PropertyDecorator { + return (_target: unknown, _property: string | number | symbol) => { + // this is a no-op that gets replaced during the build process using the transformer below + // Throw an error when this doesn't get transformed + throw new Error( + "validateArgs is a compile-time decorator and must be compiled with a transformer", + ); + }; +} + +import transformer from "./validateArgs/transformer"; +export default transformer; diff --git a/packages/transformers/src/validateArgs/README.md b/packages/transformers/src/validateArgs/README.md new file mode 100644 index 000000000000..f62d062b268c --- /dev/null +++ b/packages/transformers/src/validateArgs/README.md @@ -0,0 +1,18 @@ +# validateArgs + +TypeScript transformer that generates run-time type-checks, based on https://github.com/woutervh-/typescript-is + +Usage: + +```ts +import { validateArgs } from "@zwave-js/transformers"; + +class Test { + @validateArgs() + foo(arg1: number, arg2: Foo, arg3: Foo & Bar): void { + // implementation + } +} +``` + +The import and the decorator call will be removed and the function body of `foo` will be prepended with assertions for each of the arguments. diff --git a/packages/transformers/src/validateArgs/reason.ts b/packages/transformers/src/validateArgs/reason.ts new file mode 100644 index 000000000000..e12e8f017bfb --- /dev/null +++ b/packages/transformers/src/validateArgs/reason.ts @@ -0,0 +1,119 @@ +export interface ExpectedFunction { + type: "function"; +} + +export interface ExpectedString { + type: "string"; +} + +export interface ExpectedNumber { + type: "number"; +} + +export interface ExpectedBigInt { + type: "big-int"; +} + +export interface ExpectedBoolean { + type: "boolean"; +} + +export interface ExpectedStringLiteral { + type: "string-literal"; + value: string; +} + +export interface ExpectedNumberLiteral { + type: "number-literal"; + value: number; +} + +export interface ExpectedBooleanLiteral { + type: "boolean-literal"; + value: boolean; +} + +export interface ExpectedObject { + type: "object"; +} + +export interface ExpectedDate { + type: "date"; +} + +export interface ExpectedNonPrimitive { + type: "non-primitive"; +} + +export interface MissingObjectProperty { + type: "missing-property"; + property: string; +} + +export interface SuperfluousObjectProperty { + type: "superfluous-property"; +} + +export interface ExpectedObjectKeyof { + type: "object-keyof"; + properties: string[]; +} + +export interface ExpectedArray { + type: "array"; +} + +export interface NeverType { + type: "never"; +} + +export interface ExpectedTuple { + type: "tuple"; + minLength: number; + maxLength: number; +} + +export interface NoValidUnionAlternatives { + type: "union"; +} + +export interface ExpectedUndefined { + type: "undefined"; +} + +export interface ExpectedNull { + type: "null"; +} + +export type TemplateLiteralPair = [ + string, + "string" | "number" | "bigint" | "any" | "undefined" | "null" | undefined, +]; + +export interface ExpectedTemplateLiteral { + type: "template-literal"; + value: TemplateLiteralPair[]; +} + +export type Reason = + | ExpectedFunction + | ExpectedString + | ExpectedNumber + | ExpectedBigInt + | ExpectedBoolean + | ExpectedObject + | ExpectedDate + | ExpectedNonPrimitive + | MissingObjectProperty + | SuperfluousObjectProperty + | ExpectedObjectKeyof + | ExpectedArray + | ExpectedTuple + | NeverType + | NoValidUnionAlternatives + | ExpectedUndefined + | ExpectedNull + | ExpectedStringLiteral + | ExpectedNumberLiteral + | ExpectedBooleanLiteral + | ExpectedTemplateLiteral; diff --git a/packages/transformers/src/validateArgs/transform-node.ts b/packages/transformers/src/validateArgs/transform-node.ts new file mode 100644 index 000000000000..02a3ec51ae15 --- /dev/null +++ b/packages/transformers/src/validateArgs/transform-node.ts @@ -0,0 +1,611 @@ +import * as path from "path"; +import ts from "typescript"; +import { sliceMapValues } from "./utils"; +import type { + FileSpecificVisitorContext, + VisitorContext, +} from "./visitor-context"; +import { + visitShortCircuit, + visitType, + visitUndefinedOrType, +} from "./visitor-type-check"; +import * as VisitorUtils from "./visitor-utils"; + +function createArrowFunction( + type: ts.Type, + rootName: string, + optional: boolean, + partialVisitorContext: FileSpecificVisitorContext, +) { + const functionMap: VisitorContext["functionMap"] = new Map(); + const functionNames: VisitorContext["functionNames"] = new Set(); + const typeIdMap: VisitorContext["typeIdMap"] = new Map(); + const visitorContext: VisitorContext = { + ...partialVisitorContext, + functionNames, + functionMap, + typeIdMap, + }; + const f = visitorContext.factory; + const emitDetailedErrors = !!visitorContext.options.emitDetailedErrors; + const functionName = partialVisitorContext.options.shortCircuit + ? visitShortCircuit(visitorContext) + : optional + ? visitUndefinedOrType(type, visitorContext) + : visitType(type, visitorContext); + + const variableDeclarations: ts.VariableStatement[] = []; + if (emitDetailedErrors) { + variableDeclarations.push( + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + VisitorUtils.pathIdentifier, + undefined, + undefined, + f.createArrayLiteralExpression([ + f.createStringLiteral(rootName), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + } + const functionDeclarations = sliceMapValues(functionMap); + + return f.createArrowFunction( + undefined, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ), + ], + undefined, + undefined, + VisitorUtils.createBlock(f, [ + ...variableDeclarations, + ...functionDeclarations, + f.createReturnStatement( + f.createCallExpression( + f.createIdentifier(functionName), + undefined, + [VisitorUtils.objectIdentifier], + ), + ), + ]), + ); +} + +// function transformDecorator( +// node: ts.Decorator, +// parameterType: ts.Type, +// parameterName: string, +// optional: boolean, +// visitorContext: VisitorContextWithFactory, +// ): ts.Decorator { +// if (ts.isCallExpression(node.expression)) { +// const signature = visitorContext.checker.getResolvedSignature( +// node.expression, +// ); +// if ( +// signature !== undefined && +// signature.declaration !== undefined && +// VisitorUtils.getCanonicalPath( +// path.resolve(signature.declaration.getSourceFile().fileName), +// visitorContext, +// ) === +// path.resolve(path.join(__dirname, "..", "..", "index.d.ts")) && +// node.expression.arguments.length <= 1 +// ) { +// const arrowFunction: ts.Expression = createArrowFunction( +// parameterType, +// parameterName, +// optional, +// visitorContext, +// ); +// const expression = ts.updateCall( +// node.expression, +// node.expression.expression, +// undefined, +// [arrowFunction].concat(node.expression.arguments), +// ); +// return ts.updateDecorator(node, expression); +// } +// } +// return node; +// } + +function isValidateArgsDecorator( + decorator: ts.Decorator, + visitorContext: FileSpecificVisitorContext, +): boolean { + if (ts.isCallExpression(decorator.expression)) { + const signature = visitorContext.checker.getResolvedSignature( + decorator.expression, + ); + const decoratorName = decorator.expression.expression.getText( + decorator.getSourceFile(), + ); + + if ( + signature !== undefined && + signature.declaration !== undefined && + VisitorUtils.getCanonicalPath( + path.resolve(signature.declaration.getSourceFile().fileName), + visitorContext, + ) === + path.resolve(path.join(__dirname, "../../build/index.d.ts")) && + decoratorName === "validateArgs" + ) { + return true; + } + } + return false; +} + +// /** Figures out an appropriate human-readable name for the variable designated by `node`. */ +// function extractVariableName(node: ts.Node | undefined) { +// return node !== undefined && ts.isIdentifier(node) +// ? node.escapedText.toString() +// : "$"; +// } + +function transformDecoratedMethod( + method: ts.MethodDeclaration, + validateArgsDecorator: ts.Decorator, + visitorContext: FileSpecificVisitorContext, +) { + // Remove the decorator and TODO: prepend its body with the validation code + const f = visitorContext.factory; + + let body = method.body ?? f.createBlock([], true); + const newStatements: ts.Statement[] = []; + for (const param of method.parameters) { + if (!param.type) continue; + + let typeName: string | undefined; + let publicTypeName: string | undefined; + const type = visitorContext.checker.getTypeFromTypeNode(param.type); + let arrowFunction: ts.ArrowFunction; + + const optional = !!(param.initializer || param.questionToken); + + switch (param.type.kind) { + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.BigIntKeyword: + case ts.SyntaxKind.TypeReference: + // This is a type with an "easy" name we can factor out of the method body + publicTypeName = typeName = param.type.getText(); + if (optional) { + publicTypeName = `(optional) ${publicTypeName}`; + typeName = `optional_${typeName}`; + } + + arrowFunction = createArrowFunction(type, typeName, optional, { + ...visitorContext, + options: { + ...visitorContext.options, + emitDetailedErrors: false, + }, + }); + createLocalAssertExpression; + + // Fall through + + default: + // This is a type with a "complicated" name, we need to check within the function body + if (!typeName) { + const typeContext: VisitorContext = { + ...visitorContext, + options: { + ...visitorContext.options, + emitDetailedErrors: false, + }, + functionNames: new Set(), + functionMap: new Map(), + typeIdMap: new Map(), + }; + + typeName = + (optional ? "optional_" : "") + + visitType(type, typeContext); + arrowFunction = createArrowFunction( + type, + typeName, + optional, + typeContext, + ); + } + + const argName = (param.name as ts.Identifier).text; + const assertion = createLocalAssertExpression( + f, + argName, + typeName, + publicTypeName, + ); + if (!visitorContext.typeAssertions.has(typeName)) { + visitorContext.typeAssertions.set(typeName, arrowFunction!); + } + newStatements.push(assertion); + } + } + body = f.updateBlock(body, [...newStatements, ...body.statements]); + + return f.updateMethodDeclaration( + method, + method.decorators?.filter((d) => d !== validateArgsDecorator), + method.modifiers, + method.asteriskToken, + method.name, + method.questionToken, + method.typeParameters, + method.parameters, + method.type, + body, + ); +} + +export function createGenericAssertFunction( + factory: ts.NodeFactory, +): ts.FunctionDeclaration { + // Generated with https://ts-ast-viewer.com + return factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier("__assertType"), + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier("argName"), + undefined, + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier("typeName"), + undefined, + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + factory.createKeywordTypeNode( + ts.SyntaxKind.UndefinedKeyword, + ), + ]), + undefined, + ), + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier("boundHasError"), + undefined, + factory.createFunctionTypeNode( + undefined, + [], + factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + ), + undefined, + ), + ], + undefined, + factory.createBlock( + [ + // require the Error types here, so we don't depend on any potentially existing imports + // Additionally, imports don't seem to be matched to the usage here, so we avoid further problems + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createObjectBindingPattern([ + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier("ZWaveError"), + undefined, + ), + factory.createBindingElement( + undefined, + undefined, + factory.createIdentifier( + "ZWaveErrorCodes", + ), + undefined, + ), + ]), + undefined, + undefined, + factory.createCallExpression( + factory.createIdentifier("require"), + undefined, + [ + factory.createStringLiteral( + "@zwave-js/core", + ), + ], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + factory.createIfStatement( + factory.createCallExpression( + factory.createIdentifier("boundHasError"), + undefined, + [], + ), + factory.createBlock( + [ + factory.createThrowStatement( + factory.createNewExpression( + factory.createIdentifier("ZWaveError"), + undefined, + [ + factory.createConditionalExpression( + factory.createIdentifier( + "typeName", + ), + factory.createToken( + ts.SyntaxKind.QuestionToken, + ), + factory.createTemplateExpression( + factory.createTemplateHead( + "", + "", + ), + [ + factory.createTemplateSpan( + factory.createIdentifier( + "argName", + ), + factory.createTemplateMiddle( + " is not a ", + " is not a ", + ), + ), + factory.createTemplateSpan( + factory.createIdentifier( + "typeName", + ), + factory.createTemplateTail( + "", + "", + ), + ), + ], + ), + factory.createToken( + ts.SyntaxKind.ColonToken, + ), + factory.createTemplateExpression( + factory.createTemplateHead( + "", + "", + ), + [ + factory.createTemplateSpan( + factory.createIdentifier( + "argName", + ), + factory.createTemplateTail( + " has the wrong type", + " has the wrong type", + ), + ), + ], + ), + ), + factory.createPropertyAccessExpression( + factory.createIdentifier( + "ZWaveErrorCodes", + ), + factory.createIdentifier( + "Argument_Invalid", + ), + ), + ], + ), + ), + ], + true, + ), + undefined, + ), + ], + true, + ), + ); +} + +function createLocalAssertExpression( + factory: ts.NodeFactory, + argName: string, + typeName: string, + publicTypeName: string | undefined, +): ts.ExpressionStatement { + return factory.createExpressionStatement( + factory.createCallExpression( + factory.createIdentifier("__assertType"), + undefined, + [ + factory.createStringLiteral(argName), + publicTypeName + ? factory.createStringLiteral(publicTypeName) + : factory.createIdentifier("undefined"), + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier(`__assertType__${typeName}`), + factory.createIdentifier("bind"), + ), + undefined, + [ + factory.createVoidExpression( + factory.createNumericLiteral("0"), + ), + factory.createIdentifier(argName), + ], + ), + ], + ), + ); +} + +export function transformNode( + node: ts.Node, + visitorContext: FileSpecificVisitorContext, +): ts.Node { + const f = visitorContext.factory; + if (ts.isMethodDeclaration(node) && node.decorators?.length) { + // @validateArgs() + const validateArgsDecorator = node.decorators.find((d) => + isValidateArgsDecorator(d, visitorContext), + ); + if (validateArgsDecorator) { + // This is a method which was decorated with @validateArgs + return transformDecoratedMethod( + node, + validateArgsDecorator, + visitorContext, + ); + } + // } else if (ts.isCallExpression(node)) { + // // is(), createIs() + // const signature = visitorContext.checker.getResolvedSignature(node); + // if ( + // signature !== undefined && + // signature.declaration !== undefined && + // VisitorUtils.getCanonicalPath( + // path.resolve(signature.declaration.getSourceFile().fileName), + // visitorContext, + // ) === + // path.resolve(path.join(__dirname, "../../build/index.d.ts")) && + // node.typeArguments !== undefined && + // node.typeArguments.length === 1 + // ) { + // // const name = visitorContext.checker.getTypeAtLocation( + // // signature.declaration, + // // ).symbol.name; + // const isAssert = false; + // // name === "assertType" || name === "createAssertType"; + // const emitDetailedErrors = + // visitorContext.options.emitDetailedErrors === "auto" + // ? isAssert + // : visitorContext.options.emitDetailedErrors; + + // const typeArgument = node.typeArguments[0]; + // const type = + // visitorContext.checker.getTypeFromTypeNode(typeArgument); + // const arrowFunction = createArrowFunction( + // type, + // extractVariableName(node.arguments[0]), + // false, + // { + // ...visitorContext, + // options: { + // ...visitorContext.options, + // emitDetailedErrors, + // }, + // }, + // ); + + // return ts.updateCall(node, node.expression, node.typeArguments, [ + // ...node.arguments, + // arrowFunction, + // ]); + // } + } else if ( + visitorContext.options.transformNonNullExpressions && + ts.isNonNullExpression(node) + ) { + const expression = node.expression; + return f.updateNonNullExpression( + node, + f.createParenthesizedExpression( + f.createConditionalExpression( + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createTypeOfExpression(expression), + f.createToken( + ts.SyntaxKind.EqualsEqualsEqualsToken, + ), + f.createStringLiteral("undefined"), + ), + f.createToken(ts.SyntaxKind.BarBarToken), + f.createBinaryExpression( + expression, + f.createToken( + ts.SyntaxKind.EqualsEqualsEqualsToken, + ), + f.createNull(), + ), + ), + ), + f.createToken(ts.SyntaxKind.QuestionToken), + f.createCallExpression( + f.createParenthesizedExpression( + f.createArrowFunction( + undefined, + undefined, + [], + undefined, + f.createToken( + ts.SyntaxKind.EqualsGreaterThanToken, + ), + VisitorUtils.createBlock(f, [ + f.createThrowStatement( + f.createNewExpression( + f.createIdentifier("Error"), + undefined, + [ + f.createTemplateExpression( + f.createTemplateHead( + `${expression.getText()} was non-null asserted but is `, + ), + [ + f.createTemplateSpan( + expression, + f.createTemplateTail( + "", + ), + ), + ], + ), + ], + ), + ), + ]), + ), + ), + undefined, + [], + ), + f.createToken(ts.SyntaxKind.ColonToken), + expression, + ), + ), + ); + } + return node; +} diff --git a/packages/transformers/src/validateArgs/transformer.ts b/packages/transformers/src/validateArgs/transformer.ts new file mode 100644 index 000000000000..1df49408e7e6 --- /dev/null +++ b/packages/transformers/src/validateArgs/transformer.ts @@ -0,0 +1,182 @@ +import ts from "typescript"; +import { createGenericAssertFunction, transformNode } from "./transform-node"; +import type { + FileSpecificVisitorContext, + PartialVisitorContext, +} from "./visitor-context"; + +function getEmitDetailedErrors(options?: { + [Key: string]: unknown; +}): PartialVisitorContext["options"]["emitDetailedErrors"] { + if (options) { + if ( + options.emitDetailedErrors === "auto" || + typeof options.emitDetailedErrors === "boolean" + ) { + return options.emitDetailedErrors; + } + } + return "auto"; +} + +export default function transformer( + program: ts.Program, + options?: { [Key: string]: unknown }, +): ts.TransformerFactory { + if (options?.verbose) { + console.log( + `@zwave-js/transformer: transforming program with ${ + program.getSourceFiles().length + } source files; using TypeScript ${ts.version}.`, + ); + } + + const visitorContext: PartialVisitorContext = { + program, + checker: program.getTypeChecker(), + compilerOptions: program.getCompilerOptions(), + options: { + shortCircuit: !!options?.shortCircuit, + transformNonNullExpressions: !!options?.transformNonNullExpressions, + emitDetailedErrors: getEmitDetailedErrors(options), + }, + typeMapperStack: [], + previousTypeReference: null, + canonicalPaths: new Map(), + }; + return (context: ts.TransformationContext) => (file: ts.SourceFile) => { + // Bail early if there is no import for "@zwave-js/transformers". In this case, there's nothing to transform + if (file.getFullText().indexOf("@zwave-js/transformers") === -1) { + if (options?.verbose) { + console.log( + `@zwave-js/transformers not imported in ${file.fileName}, skipping`, + ); + } + return file; + } + + const factory = context.factory; + const fileVisitorContext: FileSpecificVisitorContext = { + ...visitorContext, + factory, + typeAssertions: new Map(), + }; + file = transformNodeAndChildren( + file, + program, + context, + fileVisitorContext, + ); + + // Remove @zwave-js/transformers import + const selfImports = file.statements + .filter((s): s is ts.ImportDeclaration => ts.isImportDeclaration(s)) + .filter( + (i) => + i.moduleSpecifier + .getText(file) + .replace(/^["']|["']$/g, "") === + "@zwave-js/transformers", + ); + if (selfImports.length > 0) { + file = context.factory.updateSourceFile( + file, + file.statements.filter((s) => !selfImports.includes(s as any)), + file.isDeclarationFile, + file.referencedFiles, + file.typeReferenceDirectives, + file.hasNoDefaultLib, + file.libReferenceDirectives, + ); + } + + if (fileVisitorContext.typeAssertions.size > 0) { + // Add top-level declarations + const newStatements: ts.Statement[] = []; + + // Generic assert function used by all assertions + newStatements.push(createGenericAssertFunction(factory)); + // And the individual "named" assertions + for (const [ + typeName, + assertion, + ] of fileVisitorContext.typeAssertions) { + newStatements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier( + `__assertType__${typeName}`, + ), + undefined, + undefined, + assertion, + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + } + + file = context.factory.updateSourceFile( + file, + [...newStatements, ...file.statements], + file.isDeclarationFile, + file.referencedFiles, + file.typeReferenceDirectives, + file.hasNoDefaultLib, + file.libReferenceDirectives, + ); + } + + return file; + }; +} + +function transformNodeAndChildren( + node: ts.SourceFile, + program: ts.Program, + context: ts.TransformationContext, + visitorContext: FileSpecificVisitorContext, +): ts.SourceFile; +function transformNodeAndChildren( + node: ts.Node, + program: ts.Program, + context: ts.TransformationContext, + visitorContext: FileSpecificVisitorContext, +): ts.Node; +function transformNodeAndChildren( + node: ts.Node, + program: ts.Program, + context: ts.TransformationContext, + visitorContext: FileSpecificVisitorContext, +): ts.Node { + let transformedNode: ts.Node; + try { + transformedNode = transformNode(node, visitorContext); + } catch (error: any) { + const sourceFile = node.getSourceFile(); + const { line, character } = sourceFile.getLineAndCharacterOfPosition( + node.pos, + ); + throw new Error( + `Failed to transform node at: ${sourceFile.fileName}:${line + 1}:${ + character + 1 + }: ${error.stack}`, + ); + } + return ts.visitEachChild( + transformedNode, + (childNode) => + transformNodeAndChildren( + childNode, + program, + context, + visitorContext, + ), + context, + ); +} diff --git a/packages/transformers/src/validateArgs/utils.ts b/packages/transformers/src/validateArgs/utils.ts new file mode 100644 index 000000000000..39e6eeac5b2a --- /dev/null +++ b/packages/transformers/src/validateArgs/utils.ts @@ -0,0 +1,19 @@ +export function sliceSet(set: Set): T[] { + const items: T[] = []; + set.forEach((value) => items.push(value)); + return items; +} + +export function setIntersection(set1: Set, set2: Set): Set { + return new Set(sliceSet(set1).filter((x) => set2.has(x))); +} + +export function setUnion(set1: Set, set2: Set): Set { + return new Set([...sliceSet(set1), ...sliceSet(set2)]); +} + +export function sliceMapValues(map: Map): U[] { + const items: U[] = []; + map.forEach((value) => items.push(value)); + return items; +} diff --git a/packages/transformers/src/validateArgs/visitor-context.ts b/packages/transformers/src/validateArgs/visitor-context.ts new file mode 100644 index 000000000000..a85e0db4471d --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-context.ts @@ -0,0 +1,28 @@ +import type ts from "typescript"; + +interface Options { + shortCircuit: boolean; + transformNonNullExpressions: boolean; + emitDetailedErrors: boolean | "auto"; +} + +export interface VisitorContext extends FileSpecificVisitorContext { + functionNames: Set; + functionMap: Map; + typeIdMap: Map; +} + +export interface FileSpecificVisitorContext extends PartialVisitorContext { + factory: ts.NodeFactory; + typeAssertions: Map; +} + +export interface PartialVisitorContext { + program: ts.Program; + checker: ts.TypeChecker; + compilerOptions: ts.CompilerOptions; + options: Options; + typeMapperStack: Map[]; + previousTypeReference: ts.Type | null; + canonicalPaths: Map; +} diff --git a/packages/transformers/src/validateArgs/visitor-indexed-access.ts b/packages/transformers/src/validateArgs/visitor-indexed-access.ts new file mode 100644 index 000000000000..e130a169e66b --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-indexed-access.ts @@ -0,0 +1,371 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import { sliceSet } from "./utils"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorIsNumber from "./visitor-is-number"; +import * as VisitorIsString from "./visitor-is-string"; +import * as VisitorTypeCheck from "./visitor-type-check"; +import * as VisitorTypeName from "./visitor-type-name"; +import * as VisitorUtils from "./visitor-utils"; + +function visitRegularObjectType( + type: ts.ObjectType, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "indexed-access", + indexType, + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + // TODO: check property index + // const stringIndexType = visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.String); + + const properties = visitorContext.checker.getPropertiesOfType(type); + const propertiesInfo = properties.map((property) => + VisitorUtils.getPropertyInfo(type, property, visitorContext), + ); + const stringType = VisitorIsString.visitType(indexType, visitorContext); + if (typeof stringType === "boolean") { + if (!stringType) { + throw new Error( + "A non-string type was used to index an object type.", + ); + } + const functionNames = propertiesInfo.map((propertyInfo) => + propertyInfo.isMethod + ? VisitorUtils.getIgnoredTypeFunction(visitorContext) + : VisitorTypeCheck.visitType( + propertyInfo.type!, + visitorContext, + ), + ); + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } else { + const strings = sliceSet(stringType); + if ( + strings.some((value) => + propertiesInfo.every( + (propertyInfo) => propertyInfo.name !== value, + ), + ) + ) { + throw new Error( + "Indexed access on object type with an index that does not exist.", + ); + } + const stringPropertiesInfo = strings.map( + (value) => + propertiesInfo.find( + (propertyInfo) => propertyInfo.name === value, + )!, + ); + const functionNames = stringPropertiesInfo.map((propertyInfo) => + propertyInfo.isMethod + ? VisitorUtils.getIgnoredTypeFunction(visitorContext) + : VisitorTypeCheck.visitType( + propertyInfo.type!, + visitorContext, + ), + ); + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } + }); +} + +function visitTupleObjectType( + type: ts.TupleType, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "indexed-access", + indexType, + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + if (type.typeArguments === undefined) { + throw new Error("Expected tuple type to have type arguments."); + } + const numberType = VisitorIsNumber.visitType(indexType, visitorContext); + if (typeof numberType === "boolean") { + if (!numberType) { + throw new Error( + "A non-number type was used to index a tuple type.", + ); + } + const functionNames = type.typeArguments.map((type) => + VisitorTypeCheck.visitType(type, visitorContext), + ); + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } else { + const numbers = sliceSet(numberType); + if (numbers.some((value) => value >= type.typeArguments!.length)) { + throw new Error( + "Indexed access on tuple type exceeds length of tuple.", + ); + } + const functionNames = numbers.map((value) => + VisitorTypeCheck.visitType( + type.typeArguments![value], + visitorContext, + ), + ); + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } + }); +} + +function visitArrayObjectType( + type: ts.ObjectType, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const numberIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.Number, + ); + if (numberIndexType === undefined) { + throw new Error( + "Expected array ObjectType to have a number index type.", + ); + } + const numberType = VisitorIsNumber.visitType(indexType, visitorContext); + if (numberType !== false) { + return VisitorTypeCheck.visitType(numberIndexType, visitorContext); + } else { + throw new Error("A non-number type was used to index an array type."); + } +} + +function visitObjectType( + type: ts.ObjectType, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(type, indexType, visitorContext); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(type, indexType, visitorContext); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(type, indexType, visitorContext); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "indexed-access", + indexType, + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const functionNames = type.types.map((type) => + visitType(type, indexType, visitorContext), + ); + if (tsutils.isUnionType(type)) { + // (T | U)[I] = T[I] & U[I] + return VisitorUtils.createConjunctionFunction(functionNames, name); + } else { + // (T & U)[I] = T[I] | U[I] + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } + }); +} + +function visitIndexType(): string { + // (keyof U)[T] is an error (actually it can be String.toString or String.valueOf but we don't support this edge case) + throw new Error("Index types cannot be used as indexed types."); +} + +function visitNonPrimitiveType(): string { + // object[T] is an error + throw new Error("Non-primitive object cannot be used as an indexed type."); +} + +function visitLiteralType(): string { + // 'string'[T] or 0xFF[T] is an error + throw new Error( + "Literal strings/numbers cannot be used as an indexed type.", + ); +} + +function visitTypeReference( + type: ts.TypeReference, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, indexType, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter( + type: ts.Type, + indexType: ts.Type, + visitorContext: VisitorContext, +) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, indexType, visitorContext); +} + +function visitBigInt(): string { + // bigint[T] is an error + throw new Error("BigInt cannot be used as an indexed type."); +} + +function visitBoolean(): string { + // boolean[T] is an error + throw new Error("Boolean cannot be used as an indexed type."); +} + +function visitString(): string { + // string[T] is an error + throw new Error("String cannot be used as an indexed type."); +} + +function visitBooleanLiteral(): string { + // true[T] or false[T] is an error + throw new Error("True/false cannot be used as an indexed type."); +} + +function visitNumber(): string { + // number[T] is an error + throw new Error("Number cannot be used as an indexed type."); +} + +function visitUndefined(): string { + // undefined[T] is an error + throw new Error("Undefined cannot be used as an indexed type."); +} + +function visitNull(): string { + // null[T] is an error + throw new Error("Null cannot be used as an indexed type."); +} + +function visitNever(visitorContext: VisitorContext) { + // never[T] = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitUnknown(visitorContext: VisitorContext) { + // unknown[T] = unknown + return VisitorUtils.getUnknownFunction(visitorContext); +} + +function visitAny(visitorContext: VisitorContext) { + // any[T] = any + return VisitorUtils.getAnyFunction(visitorContext); +} + +export function visitType( + type: ts.Type, + indexType: ts.Type, + visitorContext: VisitorContext, +): string { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(visitorContext); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(visitorContext); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(visitorContext); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, indexType, visitorContext); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, indexType, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, indexType, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, indexType, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + // return visitIndexedAccessType(type, visitorContext); + // TODO: + throw new Error("Not yet implemented."); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} diff --git a/packages/transformers/src/validateArgs/visitor-is-number.ts b/packages/transformers/src/validateArgs/visitor-is-number.ts new file mode 100644 index 000000000000..d78e5adbd8c6 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-is-number.ts @@ -0,0 +1,232 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorUtils from "./visitor-utils"; + +function visitRegularObjectType() { + return false; +} + +function visitTupleObjectType() { + return false; +} + +function visitArrayObjectType() { + return false; +} + +function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext) { + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, +) { + const numberTypes = type.types.map((type) => + visitType(type, visitorContext), + ); + + if (tsutils.isUnionType(type)) { + if (numberTypes.some((numberType) => numberType === false)) { + return false; + } + if (numberTypes.some((numberType) => numberType === true)) { + return true; + } + const numbers: Set = new Set(); + for (const numberType of numberTypes) { + for (const value of numberType as Set) { + numbers.add(value); + } + } + return numbers; + } else { + const numbers: Set = new Set(); + for (const numberType of numberTypes) { + if (typeof numberType !== "boolean") { + for (const value of numberType) { + numbers.add(value); + } + } + } + if (numbers.size === 1) { + return numbers; + } + if (numbers.size > 1) { + return false; + } + if (numberTypes.some((numberType) => numberType === true)) { + return true; + } + return false; + } +} + +function visitIndexType(): boolean { + // TODO: implement a visitor that checks if the index type is an array/tuple, then this can be a number. + throw new Error("Not yet implemented."); +} + +function visitNonPrimitiveType() { + return false; +} + +function visitLiteralType(type: ts.LiteralType) { + if (typeof type.value === "string") { + return false; + } else if (typeof type.value === "number") { + return new Set([type.value]); + } else { + throw new Error("Type value is expected to be a string or number."); + } +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter(type: ts.Type, visitorContext: VisitorContext) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext); +} + +function visitBigInt() { + return false; +} + +function visitBoolean() { + return false; +} + +function visitString() { + return false; +} + +function visitBooleanLiteral() { + return false; +} + +function visitNumber() { + return true; +} + +function visitUndefined() { + return false; +} + +function visitNull() { + return false; +} + +function visitNever() { + return false; +} + +function visitUnknown() { + return false; +} + +function visitAny() { + return true; +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, +): Set | boolean { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, visitorContext); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(type); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + // return visitIndexedAccessType(type, visitorContext); + // TODO: + throw new Error("Not yet implemented."); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} diff --git a/packages/transformers/src/validateArgs/visitor-is-string-keyof.ts b/packages/transformers/src/validateArgs/visitor-is-string-keyof.ts new file mode 100644 index 000000000000..55b6ac229e76 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-is-string-keyof.ts @@ -0,0 +1,262 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import { setIntersection, setUnion } from "./utils"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorUtils from "./visitor-utils"; + +function visitRegularObjectType(type: ts.Type, visitorContext: VisitorContext) { + const stringIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.String, + ); + if (stringIndexType) { + return true; + } + const properties = visitorContext.checker.getPropertiesOfType(type); + const propertiesInfo = properties.map((property) => + VisitorUtils.getPropertyInfo(type, property, visitorContext), + ); + const propertiesName = propertiesInfo.map( + (propertyInfo) => propertyInfo.name, + ); + return new Set(propertiesName); +} + +function visitTupleObjectType() { + return false; +} + +function visitArrayObjectType() { + return false; +} + +function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext) { + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(type, visitorContext); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, +) { + const stringTypes = type.types.map((type) => + visitType(type, visitorContext), + ); + + if (tsutils.isUnionType(type)) { + // keyof (T | U) = (keyof T) & (keyof U) + if (stringTypes.some((stringType) => stringType === false)) { + // If keyof T or keyof U is not assignable to string then keyof T & keyof U is not assignable to string. + return false; + } + if (stringTypes.some((stringType) => stringType !== true)) { + // Some keyof T or keyof U is a union of specific string literals. + const stringSets = stringTypes.filter( + (stringType) => stringType !== true, + ) as Set[]; + let strings = stringSets[0]; + for (let i = 1; i < stringSets.length; i++) { + strings = setIntersection(strings, stringSets[i]); + } + return strings; + } else { + // Both keyof T and keyof U are the string type. + return true; + } + } else { + // keyof (T & U) = (keyof T) | (keyof U) + if (stringTypes.some((stringType) => stringType === true)) { + // If keyof T or keyof U is the string type then keyof T | keyof U is assignable to the string type. + return true; + } + if (stringTypes.some((stringType) => stringType !== false)) { + const stringSets = stringTypes.filter( + (stringType) => stringType !== false, + ) as Set[]; + let strings = stringSets[0]; + for (let i = 1; i < stringSets.length; i++) { + strings = setUnion(strings, stringSets[i]); + } + return strings; + } else { + // Both keyof T and keyof U are not assignable to the string type. + return false; + } + } +} + +function visitIndexType(): boolean { + // TODO: implement a visitor that checks if the index type is an object, then this can be a string. + throw new Error("Not yet implemented."); +} + +function visitNonPrimitiveType() { + return false; +} + +function visitLiteralType(type: ts.LiteralType) { + if (typeof type.value === "string") { + return false; + } else if (typeof type.value === "number") { + return false; + } else { + throw new Error("Type value is expected to be a string or number."); + } +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter(type: ts.Type, visitorContext: VisitorContext) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext); +} + +function visitBigInt() { + return false; +} + +function visitBoolean() { + // keyof boolean + return false; +} + +function visitString() { + // keyof string + return false; +} + +function visitBooleanLiteral() { + // keyof true/keyof false + return false; +} + +function visitNumber() { + // keyof number + return false; +} + +function visitUndefined() { + // keyof undefined + return false; +} + +function visitNull() { + // keyof null + return false; +} + +function visitNever() { + // keyof never + return false; +} + +function visitUnknown() { + // keyof unknown + return false; +} + +function visitAny() { + // keyof any + return true; +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, +): Set | boolean { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, visitorContext); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(type); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + // return visitIndexedAccessType(type, visitorContext); + // TODO: + throw new Error("Not yet implemented."); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} diff --git a/packages/transformers/src/validateArgs/visitor-is-string.ts b/packages/transformers/src/validateArgs/visitor-is-string.ts new file mode 100644 index 000000000000..8c4c619b90a9 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-is-string.ts @@ -0,0 +1,242 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import { setIntersection, setUnion } from "./utils"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorIsStringKeyof from "./visitor-is-string-keyof"; +import * as VisitorUtils from "./visitor-utils"; + +function visitRegularObjectType() { + return false; +} + +function visitTupleObjectType() { + return false; +} + +function visitArrayObjectType() { + return false; +} + +function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext) { + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, +) { + const stringTypes = type.types.map((type) => + visitType(type, visitorContext), + ); + + if (tsutils.isUnionType(type)) { + if (stringTypes.some((stringType) => stringType === true)) { + // If T or U is the string type, then T | U is assignable to the string type. + return true; + } + if (stringTypes.some((stringType) => stringType !== false)) { + // Some T or U is a union of specific string literals. + const stringSets = stringTypes.filter( + (stringType) => stringType !== false, + ) as Set[]; + let strings = stringSets[0]; + for (let i = 1; i < stringSets.length; i++) { + strings = setUnion(strings, stringSets[i]); + } + return strings; + } else { + // Both T and U are not assignable to string. + return false; + } + } else { + if (stringTypes.some((stringType) => stringType === false)) { + // If T or U is not assignable to stirng, then T & U is not assignable to string. + return false; + } + if (stringTypes.some((stringType) => stringType !== true)) { + // Some T or U is a union of specific string literals. + const stringSets = stringTypes.filter( + (stringType) => stringType !== true, + ) as Set[]; + let strings = stringSets[0]; + for (let i = 1; i < stringSets.length; i++) { + strings = setIntersection(strings, stringSets[i]); + } + return strings; + } else { + // Both T and U are assignable to string. + return true; + } + } +} + +function visitIndexType(type: ts.Type, visitorContext: VisitorContext) { + const indexedType = (type as { type?: ts.Type }).type; + if (indexedType === undefined) { + throw new Error("Could not get indexed type of index type."); + } + return VisitorIsStringKeyof.visitType(indexedType, visitorContext); +} + +function visitNonPrimitiveType() { + return false; +} + +function visitLiteralType(type: ts.LiteralType) { + if (typeof type.value === "string") { + return new Set([type.value]); + } else if (typeof type.value === "number") { + return false; + } else { + throw new Error("Type value is expected to be a string or number."); + } +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter(type: ts.Type, visitorContext: VisitorContext) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext); +} + +function visitBigInt() { + return false; +} + +function visitBoolean() { + return false; +} + +function visitString() { + return true; +} + +function visitBooleanLiteral() { + return false; +} + +function visitNumber() { + return false; +} + +function visitUndefined() { + return false; +} + +function visitNull() { + return false; +} + +function visitNever() { + return false; +} + +function visitUnknown() { + return false; +} + +function visitAny() { + return true; +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, +): Set | boolean { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, visitorContext); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(type); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(type, visitorContext); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + // return visitIndexedAccessType(type, visitorContext); + // TODO: + throw new Error("Not yet implemented."); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} diff --git a/packages/transformers/src/validateArgs/visitor-keyof.ts b/packages/transformers/src/validateArgs/visitor-keyof.ts new file mode 100644 index 000000000000..474066a12214 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-keyof.ts @@ -0,0 +1,268 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorTypeName from "./visitor-type-name"; +import * as VisitorUtils from "./visitor-utils"; +import { getIntrinsicName } from "./visitor-utils"; + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, +) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "keyof", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const functionNames = type.types.map((type) => + visitType(type, visitorContext), + ); + if (tsutils.isUnionType(type)) { + // keyof (T | U) = (keyof T) & (keyof U) + return VisitorUtils.createConjunctionFunction(functionNames, name); + } else { + // keyof (T & U) = (keyof T) | (keyof U) + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + } + }); +} + +function visitIndexType(visitorContext: VisitorContext) { + // keyof keyof T = never (actually it's the methods of string, but we'll ignore those since they're not serializable) + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitNonPrimitiveType(type: ts.Type, visitorContext: VisitorContext) { + const intrinsicName = getIntrinsicName(type); + if (intrinsicName === "object") { + // keyof object = never + return VisitorUtils.getNeverFunction(visitorContext); + } else { + throw new Error( + `Unsupported non-primitive with intrinsic name: ${intrinsicName}.`, + ); + } +} + +function visitLiteralType(visitorContext: VisitorContext) { + // keyof 'string' = never and keyof 0xFF = never (actually they are the methods of string and number, but we'll ignore those since they're not serializable) + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitRegularObjectType( + type: ts.ObjectType, + visitorContext: VisitorContext, +) { + const stringIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.String, + ); + if (stringIndexType) { + // There is a string index type { [Key: string]: T }. + // keyof { [Key: string]: U } = string + return VisitorUtils.getStringFunction(visitorContext); + } else { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "keyof", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + // In keyof mode we check if the object is equal to one of the property names. + // keyof { x: T } = x + const properties = visitorContext.checker.getPropertiesOfType(type); + const names = properties.map((property) => property.name); + const condition = VisitorUtils.createBinaries( + names.map((name) => + ts.createStrictInequality( + VisitorUtils.objectIdentifier, + ts.createStringLiteral(name), + ), + ), + ts.SyntaxKind.AmpersandAmpersandToken, + ts.createTrue(), + ); + return VisitorUtils.createAssertionFunction( + condition, + { type: "object-keyof", properties: names }, + name, + visitorContext, + ); + }); + } +} + +function visitTupleObjectType(visitorContext: VisitorContext) { + // keyof [U, T] = number + // TODO: actually they're only specific numbers (0, 1, 2...) + return VisitorUtils.getNumberFunction(visitorContext); +} + +function visitArrayObjectType(visitorContext: VisitorContext) { + // keyof [] = number + return VisitorUtils.getNumberFunction(visitorContext); +} + +function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext) { + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(visitorContext); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(visitorContext); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(type, visitorContext); + } +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter(type: ts.Type, visitorContext: VisitorContext) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext); +} + +function visitBoolean(visitorContext: VisitorContext) { + // keyof boolean = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitString(visitorContext: VisitorContext) { + // keyof string = never (actually it's all the methods of string, but we'll ignore those since they're not serializable) + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitBooleanLiteral(visitorContext: VisitorContext) { + // keyof true = never and keyof false = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitBigInt(visitorContext: VisitorContext) { + // keyof bigint = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitNumber(visitorContext: VisitorContext) { + // keyof number = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitUndefined(visitorContext: VisitorContext) { + // keyof undefined = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitNull(visitorContext: VisitorContext) { + // keyof null = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitNever(visitorContext: VisitorContext) { + // keyof never = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitUnknown(visitorContext: VisitorContext) { + // keyof unknown = never + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitAny(visitorContext: VisitorContext) { + // keyof any = string (or symbol or number but we'll ignore those since they're not serializable) + return VisitorUtils.getStringFunction(visitorContext); +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, +): string { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(visitorContext); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(visitorContext); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(visitorContext); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(visitorContext); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(visitorContext); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(visitorContext); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(visitorContext); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(visitorContext); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(visitorContext); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(visitorContext); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, visitorContext); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(visitorContext); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(type, visitorContext); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(visitorContext); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + // return visitIndexedAccessType(type, visitorContext); + // TODO: + throw new Error("Not yet implemented."); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} diff --git a/packages/transformers/src/validateArgs/visitor-type-check.ts b/packages/transformers/src/validateArgs/visitor-type-check.ts new file mode 100644 index 000000000000..5b102a7562fd --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-type-check.ts @@ -0,0 +1,1739 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorIndexedAccess from "./visitor-indexed-access"; +import * as VisitorKeyof from "./visitor-keyof"; +import * as VisitorTypeName from "./visitor-type-name"; +import * as VisitorUtils from "./visitor-utils"; + +function visitDateType(type: ts.ObjectType, visitorContext: VisitorContext) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + const f = visitorContext.factory; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier("nativeDateObject"), + undefined, + undefined, + ), + ], + ts.NodeFlags.Let, + ), + ), + f.createIfStatement( + f.createBinaryExpression( + f.createTypeOfExpression(f.createIdentifier("global")), + f.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + f.createStringLiteral("undefined"), + ), + f.createExpressionStatement( + f.createBinaryExpression( + f.createIdentifier("nativeDateObject"), + f.createToken(ts.SyntaxKind.EqualsToken), + f.createPropertyAccessExpression( + f.createIdentifier("window"), + f.createIdentifier("Date"), + ), + ), + ), + f.createExpressionStatement( + f.createBinaryExpression( + f.createIdentifier("nativeDateObject"), + f.createToken(ts.SyntaxKind.EqualsToken), + f.createPropertyAccessExpression( + f.createIdentifier("global"), + f.createIdentifier("Date"), + ), + ), + ), + ), + f.createIfStatement( + f.createLogicalNot( + f.createBinaryExpression( + f.createIdentifier("object"), + f.createToken(ts.SyntaxKind.InstanceOfKeyword), + f.createIdentifier("nativeDateObject"), + ), + ), + f.createReturnStatement( + VisitorUtils.createErrorObject( + { type: "date" }, + visitorContext, + ), + ), + f.createReturnStatement(f.createNull()), + ), + ]), + ); + }); +} + +function createRecursiveCall( + functionName: string, + functionArgument: ts.Expression, + pathExpression: ts.Expression, + visitorContext: VisitorContext, +): ts.Statement[] { + const f = visitorContext.factory; + const errorIdentifier = f.createIdentifier("error"); + const emitDetailedErrors = !!visitorContext.options.emitDetailedErrors; + + const statements: ts.Statement[] = []; + if (emitDetailedErrors) { + statements.push( + f.createExpressionStatement( + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.pathIdentifier, + "push", + ), + undefined, + [ + VisitorUtils.createBinaries( + [pathExpression], + ts.SyntaxKind.PlusToken, + ), + ], + ), + ), + ); + } + statements.push( + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + errorIdentifier, + undefined, + undefined, + f.createCallExpression( + f.createIdentifier(functionName), + undefined, + [functionArgument], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + if (emitDetailedErrors) { + statements.push( + f.createExpressionStatement( + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.pathIdentifier, + "pop", + ), + undefined, + undefined, + ), + ), + ); + } + statements.push( + f.createIfStatement( + errorIdentifier, + f.createReturnStatement(errorIdentifier), + ), + ); + return statements; +} + +function visitTupleObjectType( + type: ts.TupleType, + visitorContext: VisitorContext, +) { + const f = visitorContext.factory; + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const functionNames = type.typeArguments + ? type.typeArguments.map((type) => visitType(type, visitorContext)) + : []; + + const maxLength = functionNames.length; + let minLength = functionNames.length; + for (let i = 0; i < functionNames.length; i++) { + const property = type.getProperty(i.toString()); + if (property && (property.flags & ts.SymbolFlags.Optional) !== 0) { + minLength = i; + break; + } + } + + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + f.createIfStatement( + VisitorUtils.createBinaries( + [ + f.createLogicalNot( + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("Array"), + "isArray", + ), + undefined, + [VisitorUtils.objectIdentifier], + ), + ), + f.createBinaryExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + "length", + ), + ts.SyntaxKind.LessThanToken, + f.createNumericLiteral(minLength.toString()), + ), + f.createBinaryExpression( + f.createNumericLiteral(maxLength.toString()), + ts.SyntaxKind.LessThanToken, + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + "length", + ), + ), + ], + ts.SyntaxKind.BarBarToken, + ), + f.createReturnStatement( + VisitorUtils.createErrorObject( + { type: "tuple", minLength, maxLength }, + visitorContext, + ), + ), + ), + ...functionNames.map((functionName, index) => + VisitorUtils.createBlock( + f, + createRecursiveCall( + functionName, + f.createElementAccessExpression( + VisitorUtils.objectIdentifier, + index, + ), + f.createStringLiteral(`[${index}]`), + visitorContext, + ), + ), + ), + f.createReturnStatement(f.createNull()), + ]), + ); + }); +} + +function visitArrayObjectType( + type: ts.ObjectType, + visitorContext: VisitorContext, +) { + const f = visitorContext.factory; + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const numberIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.Number, + ); + if (numberIndexType === undefined) { + throw new Error( + "Expected array ObjectType to have a number index type.", + ); + } + const functionName = visitType(numberIndexType, visitorContext); + const indexIdentifier = f.createIdentifier("i"); + + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + f.createIfStatement( + f.createLogicalNot( + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("Array"), + "isArray", + ), + undefined, + [VisitorUtils.objectIdentifier], + ), + ), + f.createReturnStatement( + VisitorUtils.createErrorObject( + { type: "array" }, + visitorContext, + ), + ), + ), + f.createForStatement( + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + indexIdentifier, + undefined, + undefined, + f.createNumericLiteral("0"), + ), + ], + ts.NodeFlags.Let, + ), + f.createBinaryExpression( + indexIdentifier, + ts.SyntaxKind.LessThanToken, + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + "length", + ), + ), + f.createPostfixIncrement(indexIdentifier), + VisitorUtils.createBlock( + f, + createRecursiveCall( + functionName, + f.createElementAccessExpression( + VisitorUtils.objectIdentifier, + indexIdentifier, + ), + VisitorUtils.createBinaries( + [ + f.createStringLiteral("["), + indexIdentifier, + f.createStringLiteral("]"), + ], + ts.SyntaxKind.PlusToken, + ), + visitorContext, + ), + ), + ), + f.createReturnStatement(f.createNull()), + ]), + ); + }); +} + +function visitRegularObjectType( + type: ts.ObjectType, + visitorContext: VisitorContext, +) { + const f = visitorContext.factory; + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const propertyInfos = visitorContext.checker + .getPropertiesOfType(type) + .map((property) => + VisitorUtils.getPropertyInfo(type, property, visitorContext), + ); + const stringIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.String, + ); + const stringIndexFunctionName = stringIndexType + ? visitType(stringIndexType, visitorContext) + : undefined; + const keyIdentifier = f.createIdentifier("key"); + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + f.createIfStatement( + VisitorUtils.createBinaries( + [ + f.createStrictInequality( + f.createTypeOfExpression( + VisitorUtils.objectIdentifier, + ), + f.createStringLiteral("object"), + ), + f.createStrictEquality( + VisitorUtils.objectIdentifier, + f.createNull(), + ), + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("Array"), + "isArray", + ), + undefined, + [VisitorUtils.objectIdentifier], + ), + ], + ts.SyntaxKind.BarBarToken, + ), + f.createReturnStatement( + VisitorUtils.createErrorObject( + { type: "object" }, + visitorContext, + ), + ), + ), + ...propertyInfos + .map((propertyInfo) => { + if (propertyInfo.isSymbol) { + return []; + } + const functionName = + propertyInfo.isMethod || propertyInfo.isFunction + ? VisitorUtils.getIgnoredTypeFunction( + visitorContext, + ) + : visitType(propertyInfo.type!, visitorContext); + return [ + f.createIfStatement( + f.createBinaryExpression( + f.createStringLiteral(propertyInfo.name), + ts.SyntaxKind.InKeyword, + VisitorUtils.objectIdentifier, + ), + VisitorUtils.createBlock( + f, + createRecursiveCall( + functionName, + f.createElementAccessExpression( + VisitorUtils.objectIdentifier, + f.createStringLiteral( + propertyInfo.name, + ), + ), + f.createStringLiteral( + propertyInfo.name, + ), + visitorContext, + ), + ), + propertyInfo.optional + ? undefined + : f.createReturnStatement( + VisitorUtils.createErrorObject( + { + type: "missing-property", + property: propertyInfo.name, + }, + visitorContext, + ), + ), + ), + ]; + }) + .reduce((a, b) => a.concat(b), []), + ...(stringIndexFunctionName + ? [ + f.createForOfStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + keyIdentifier, + undefined, + undefined, + ), + ], + ts.NodeFlags.Const, + ), + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("Object"), + "keys", + ), + undefined, + [VisitorUtils.objectIdentifier], + ), + VisitorUtils.createBlock( + f, + createRecursiveCall( + stringIndexFunctionName, + f.createElementAccessExpression( + VisitorUtils.objectIdentifier, + keyIdentifier, + ), + keyIdentifier, + visitorContext, + ), + ), + ), + ] + : []), + f.createReturnStatement(f.createNull()), + ]), + ); + }); +} + +function visitTypeAliasReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = + VisitorUtils.getTypeAliasMapping(type); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter(type: ts.Type, visitorContext: VisitorContext) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext); +} + +function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext) { + if (VisitorUtils.checkIsClass(type, visitorContext)) { + // Dates + if (VisitorUtils.checkIsDateClass(type)) { + return visitDateType(type, visitorContext); + } + + // TODO: impement checks for supported classes + + // Ignore all other classes + return VisitorUtils.getIgnoredTypeFunction(visitorContext); + } + if (tsutils.isTupleType(type)) { + // Tuple with finite length. + return visitTupleObjectType(type, visitorContext); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + // Index type is number -> array type. + return visitArrayObjectType(type, visitorContext); + } else if ( + "valueDeclaration" in type.symbol && + type.symbol.valueDeclaration && + (type.symbol.valueDeclaration.kind === + ts.SyntaxKind.MethodDeclaration || + type.symbol.valueDeclaration.kind === ts.SyntaxKind.FunctionType) + ) { + return VisitorUtils.getIgnoredTypeFunction(visitorContext); + } else if ( + type.symbol && + type.symbol.declarations && + type.symbol.declarations.length >= 1 && + ts.isFunctionTypeNode(type.symbol.declarations[0]) + ) { + return VisitorUtils.getIgnoredTypeFunction(visitorContext); + } else { + // Index type is string -> regular object type. + return visitRegularObjectType(type, visitorContext); + } +} + +function visitLiteralType( + type: ts.LiteralType, + visitorContext: VisitorContext, +) { + const f = visitorContext.factory; + if (typeof type.value === "string") { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + const value = type.value; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return VisitorUtils.createAssertionFunction( + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createStringLiteral(value), + ), + { type: "string-literal", value }, + name, + visitorContext, + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + ); + }); + } else if (typeof type.value === "number") { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + const value = type.value; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return VisitorUtils.createAssertionFunction( + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createNumericLiteral(value.toString()), + ), + { type: "number-literal", value }, + name, + visitorContext, + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + ); + }); + } else { + throw new Error("Type value is expected to be a string or number."); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, +) { + const typeUnion = type; + if (tsutils.isUnionType(typeUnion)) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + const functionNames = typeUnion.types.map((type) => + visitType(type, visitorContext), + ); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return VisitorUtils.createDisjunctionFunction( + functionNames, + name, + visitorContext, + ); + }); + } + const intersectionType = type; + if (tsutils.isIntersectionType(intersectionType)) { + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const functionNames = intersectionType.types.map((type) => + visitType(type, { ...visitorContext }), + ); + return VisitorUtils.createConjunctionFunction(functionNames, name); + }); + } + throw new Error( + "UnionOrIntersectionType type was neither a union nor an intersection.", + ); +} + +function visitBooleanLiteral(type: ts.Type, visitorContext: VisitorContext) { + const intrinsicName = VisitorUtils.getIntrinsicName(type); + const f = visitorContext.factory; + if (intrinsicName === "true") { + const name = "_true"; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return VisitorUtils.createAssertionFunction( + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createTrue(), + ), + { type: "boolean-literal", value: true }, + name, + visitorContext, + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + ); + }); + } else if (intrinsicName === "false") { + const name = "_false"; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + return VisitorUtils.createAssertionFunction( + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createFalse(), + ), + { type: "boolean-literal", value: false }, + name, + visitorContext, + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + ); + }); + } else { + throw new Error( + `Unsupported boolean literal with intrinsic name: ${intrinsicName}.`, + ); + } +} + +function visitNonPrimitiveType(type: ts.Type, visitorContext: VisitorContext) { + const intrinsicName = VisitorUtils.getIntrinsicName(type); + const f = visitorContext.factory; + if (intrinsicName === "object") { + const name = "_object"; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const conditions: ts.Expression[] = [ + f.createStrictInequality( + f.createTypeOfExpression(VisitorUtils.objectIdentifier), + f.createStringLiteral("boolean"), + ), + f.createStrictInequality( + f.createTypeOfExpression(VisitorUtils.objectIdentifier), + f.createStringLiteral("number"), + ), + f.createStrictInequality( + f.createTypeOfExpression(VisitorUtils.objectIdentifier), + f.createStringLiteral("string"), + ), + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createNull(), + ), + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createIdentifier("undefined"), + ), + ]; + const condition = VisitorUtils.createBinaries( + conditions, + ts.SyntaxKind.AmpersandAmpersandToken, + ); + return VisitorUtils.createAssertionFunction( + f.createLogicalNot(condition), + { type: "non-primitive" }, + name, + visitorContext, + VisitorUtils.createStrictNullCheckStatement( + VisitorUtils.objectIdentifier, + visitorContext, + ), + ); + }); + } else { + throw new Error( + `Unsupported non-primitive with intrinsic name: ${intrinsicName}.`, + ); + } +} + +function visitAny(visitorContext: VisitorContext) { + return VisitorUtils.getAnyFunction(visitorContext); +} + +function visitUnknown(visitorContext: VisitorContext) { + return VisitorUtils.getUnknownFunction(visitorContext); +} + +function visitNever(visitorContext: VisitorContext) { + return VisitorUtils.getNeverFunction(visitorContext); +} + +function visitNull(visitorContext: VisitorContext) { + return VisitorUtils.getNullFunction(visitorContext); +} + +function visitUndefined(visitorContext: VisitorContext) { + return VisitorUtils.getUndefinedFunction(visitorContext); +} + +function visitNumber(visitorContext: VisitorContext) { + return VisitorUtils.getNumberFunction(visitorContext); +} + +function visitBigInt(visitorContext: VisitorContext) { + return VisitorUtils.getBigIntFunction(visitorContext); +} + +function visitBoolean(visitorContext: VisitorContext) { + return VisitorUtils.getBooleanFunction(visitorContext); +} + +function visitString(visitorContext: VisitorContext) { + return VisitorUtils.getStringFunction(visitorContext); +} + +function visitIndexType(type: ts.Type, visitorContext: VisitorContext) { + // keyof T + const indexedType = (type as { type?: ts.Type }).type; + if (indexedType === undefined) { + throw new Error("Could not get indexed type of index type."); + } + return VisitorKeyof.visitType(indexedType, visitorContext); +} + +function visitIndexedAccessType( + type: ts.IndexedAccessType, + visitorContext: VisitorContext, +) { + // T[U] -> index type = U, object type = T + return VisitorIndexedAccess.visitType( + type.objectType, + type.indexType, + visitorContext, + ); +} + +function visitTemplateLiteralType( + type: ts.TemplateLiteralType, + visitorContext: VisitorContext, +) { + const f = visitorContext.factory; + const name = VisitorTypeName.visitType(type, visitorContext, { + type: "type-check", + }); + const typePairs = type.texts.reduce( + (prev, curr, i: number) => + [ + ...prev, + [ + curr, + typeof type.types[i] === "undefined" + ? undefined + : VisitorUtils.getIntrinsicName(type.types[i]), + ], + ] as never, + [] as VisitorUtils.TemplateLiteralPair[], + ); + const templateLiteralTypeError = VisitorUtils.createErrorObject( + { + type: "template-literal", + value: typePairs, + }, + visitorContext, + ); + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => + f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier("typePairs"), + undefined, + undefined, + f.createArrayLiteralExpression( + typePairs.map(([text, type]) => + f.createArrayLiteralExpression([ + f.createStringLiteral(text), + typeof type === "undefined" + ? f.createIdentifier( + "undefined", + ) + : f.createStringLiteral(type), + ]), + ), + false, + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier("position"), + undefined, + undefined, + f.createNumericLiteral("0"), + ), + ], + ts.NodeFlags.Let, + ), + ), + f.createForOfStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createArrayBindingPattern([ + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("index"), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("typePair"), + undefined, + ), + ]), + undefined, + undefined, + undefined, + ), + ], + ts.NodeFlags.Const, + ), + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("typePairs"), + f.createIdentifier("entries"), + ), + undefined, + [], + ), + VisitorUtils.createBlock(f, [ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createArrayBindingPattern([ + f.createBindingElement( + undefined, + undefined, + f.createIdentifier( + "currentText", + ), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier( + "currentType", + ), + undefined, + ), + ]), + undefined, + undefined, + f.createIdentifier("typePair"), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createArrayBindingPattern([ + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("nextText"), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("nextType"), + undefined, + ), + ]), + undefined, + undefined, + f.createBinaryExpression( + f.createElementAccessExpression( + f.createIdentifier("typePairs"), + f.createBinaryExpression( + f.createIdentifier("index"), + f.createToken( + ts.SyntaxKind.PlusToken, + ), + f.createNumericLiteral("1"), + ), + ), + f.createToken( + ts.SyntaxKind + .QuestionQuestionToken, + ), + f.createArrayLiteralExpression( + [ + f.createIdentifier( + "undefined", + ), + f.createIdentifier( + "undefined", + ), + ], + false, + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createIfStatement( + f.createBinaryExpression( + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + f.createIdentifier("substr"), + ), + undefined, + [ + f.createIdentifier("position"), + f.createPropertyAccessExpression( + f.createIdentifier("currentText"), + f.createIdentifier("length"), + ), + ], + ), + f.createToken( + ts.SyntaxKind.ExclamationEqualsEqualsToken, + ), + f.createIdentifier("currentText"), + ), + f.createReturnStatement(templateLiteralTypeError), + undefined, + ), + f.createExpressionStatement( + f.createBinaryExpression( + f.createIdentifier("position"), + f.createToken(ts.SyntaxKind.PlusEqualsToken), + f.createPropertyAccessExpression( + f.createIdentifier("currentText"), + f.createIdentifier("length"), + ), + ), + ), + f.createIfStatement( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier("nextText"), + f.createToken( + ts.SyntaxKind.EqualsEqualsEqualsToken, + ), + f.createStringLiteral(""), + ), + f.createToken( + ts.SyntaxKind.AmpersandAmpersandToken, + ), + f.createBinaryExpression( + f.createIdentifier("nextType"), + f.createToken( + ts.SyntaxKind + .ExclamationEqualsEqualsToken, + ), + f.createIdentifier("undefined"), + ), + ), + VisitorUtils.createBlock(f, [ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier("char"), + undefined, + undefined, + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + f.createIdentifier( + "charAt", + ), + ), + undefined, + [ + f.createIdentifier( + "position", + ), + ], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createIfStatement( + f.createBinaryExpression( + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "number", + ), + ), + f.createToken( + ts.SyntaxKind + .BarBarToken, + ), + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "bigint", + ), + ), + ), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createCallExpression( + f.createIdentifier("isNaN"), + undefined, + [ + f.createCallExpression( + f.createIdentifier( + "Number", + ), + undefined, + [ + f.createIdentifier( + "char", + ), + ], + ), + ], + ), + ), + ), + f.createToken( + ts.SyntaxKind.BarBarToken, + ), + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "string", + ), + ), + f.createToken( + ts.SyntaxKind + .BarBarToken, + ), + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "any", + ), + ), + ), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createBinaryExpression( + f.createIdentifier("char"), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral(""), + ), + ), + ), + ), + f.createReturnStatement( + templateLiteralTypeError, + ), + undefined, + ), + ]), + undefined, + ), + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier("nextTextOrType"), + undefined, + undefined, + f.createConditionalExpression( + f.createBinaryExpression( + f.createIdentifier("nextText"), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral(""), + ), + f.createToken( + ts.SyntaxKind.QuestionToken, + ), + f.createIdentifier("nextType"), + f.createToken( + ts.SyntaxKind.ColonToken, + ), + f.createIdentifier("nextText"), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier( + "resolvedPlaceholder", + ), + undefined, + undefined, + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + f.createIdentifier("substring"), + ), + undefined, + [ + f.createIdentifier("position"), + f.createConditionalExpression( + f.createBinaryExpression( + f.createTypeOfExpression( + f.createIdentifier( + "nextTextOrType", + ), + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "undefined", + ), + ), + f.createToken( + ts.SyntaxKind + .QuestionToken, + ), + f.createBinaryExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + f.createIdentifier( + "length", + ), + ), + f.createToken( + ts.SyntaxKind + .MinusToken, + ), + f.createNumericLiteral( + "1", + ), + ), + f.createToken( + ts.SyntaxKind + .ColonToken, + ), + f.createCallExpression( + f.createPropertyAccessExpression( + VisitorUtils.objectIdentifier, + f.createIdentifier( + "indexOf", + ), + ), + undefined, + [ + f.createIdentifier( + "nextTextOrType", + ), + f.createIdentifier( + "position", + ), + ], + ), + ), + ], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createIfStatement( + f.createBinaryExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "number", + ), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createCallExpression( + f.createIdentifier("isNaN"), + undefined, + [ + f.createCallExpression( + f.createIdentifier( + "Number", + ), + undefined, + [ + f.createIdentifier( + "resolvedPlaceholder", + ), + ], + ), + ], + ), + ), + ), + f.createToken( + ts.SyntaxKind.BarBarToken, + ), + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "bigint", + ), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier( + "resolvedPlaceholder", + ), + f.createIdentifier( + "includes", + ), + ), + undefined, + [ + f.createStringLiteral( + ".", + ), + ], + ), + f.createToken( + ts.SyntaxKind + .BarBarToken, + ), + f.createCallExpression( + f.createIdentifier( + "isNaN", + ), + undefined, + [ + f.createCallExpression( + f.createIdentifier( + "Number", + ), + undefined, + [ + f.createIdentifier( + "resolvedPlaceholder", + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + f.createToken(ts.SyntaxKind.BarBarToken), + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier( + "currentType", + ), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral( + "undefined", + ), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createBinaryExpression( + f.createIdentifier( + "resolvedPlaceholder", + ), + f.createToken( + ts.SyntaxKind + .ExclamationEqualsEqualsToken, + ), + f.createStringLiteral( + "undefined", + ), + ), + ), + ), + ), + f.createToken(ts.SyntaxKind.BarBarToken), + f.createParenthesizedExpression( + f.createBinaryExpression( + f.createBinaryExpression( + f.createIdentifier("currentType"), + f.createToken( + ts.SyntaxKind + .EqualsEqualsEqualsToken, + ), + f.createStringLiteral("null"), + ), + f.createToken( + ts.SyntaxKind + .AmpersandAmpersandToken, + ), + f.createBinaryExpression( + f.createIdentifier( + "resolvedPlaceholder", + ), + f.createToken( + ts.SyntaxKind + .ExclamationEqualsEqualsToken, + ), + f.createStringLiteral("null"), + ), + ), + ), + ), + f.createReturnStatement(templateLiteralTypeError), + undefined, + ), + f.createExpressionStatement( + f.createBinaryExpression( + f.createIdentifier("position"), + f.createToken(ts.SyntaxKind.PlusEqualsToken), + f.createPropertyAccessExpression( + f.createIdentifier("resolvedPlaceholder"), + f.createIdentifier("length"), + ), + ), + ), + ]), + ), + f.createReturnStatement(f.createNull()), + ]), + ), + ); +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, +): string { + if ((ts.TypeFlags.Any & type.flags) !== 0) { + // Any + return visitAny(visitorContext); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + // Unknown + return visitUnknown(visitorContext); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + // Never + return visitNever(visitorContext); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + // Null + return visitNull(visitorContext); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + // Undefined + return visitUndefined(visitorContext); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + // Number + return visitNumber(visitorContext); + } else if (VisitorUtils.isBigIntType(type)) { + // BigInt + return visitBigInt(visitorContext); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + // Boolean + return visitBoolean(visitorContext); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + // String + return visitString(visitorContext); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + // Boolean literal (true/false) + return visitBooleanLiteral(type, visitorContext); + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + // Type references. + return visitTypeReference(type, visitorContext); + } else if ( + type.aliasTypeArguments && + visitorContext.previousTypeReference !== type && + (type as ts.TypeReference).target + ) { + return visitTypeAliasReference( + type as ts.TypeReference, + visitorContext, + ); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + // Type parameter + return visitTypeParameter(type, visitorContext); + } else if (tsutils.isObjectType(type)) { + // Object type (including interfaces, arrays, tuples) + return visitObjectType(type, visitorContext); + } else if (tsutils.isLiteralType(type)) { + // Literal string/number types ('foo') + return visitLiteralType(type, visitorContext); + } else if (tsutils.isUnionOrIntersectionType(type)) { + // Union or intersection type (| or &) + return visitUnionOrIntersectionType(type, visitorContext); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + // Non-primitive such as object + return visitNonPrimitiveType(type, visitorContext); + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + // Index type: keyof T + return visitIndexType(type, visitorContext); + } else if (tsutils.isIndexedAccessType(type)) { + // Indexed access type: T[U] + return visitIndexedAccessType(type, visitorContext); + } else if ((ts.TypeFlags.TemplateLiteral & type.flags) !== 0) { + // template literal type: `foo${string}` + return visitTemplateLiteralType( + type as ts.TemplateLiteralType, + visitorContext, + ); + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } +} + +export function visitUndefinedOrType( + type: ts.Type, + visitorContext: VisitorContext, +): string { + const f = visitorContext.factory; + const functionName = visitType(type, visitorContext); + const name = `optional_${functionName}`; + return VisitorUtils.setFunctionIfNotExists(name, visitorContext, () => { + const errorIdentifier = f.createIdentifier("error"); + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + VisitorUtils.objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + VisitorUtils.createBlock(f, [ + f.createIfStatement( + f.createStrictInequality( + VisitorUtils.objectIdentifier, + f.createIdentifier("undefined"), + ), + VisitorUtils.createBlock(f, [ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + errorIdentifier, + undefined, + undefined, + f.createCallExpression( + f.createIdentifier(functionName), + undefined, + [VisitorUtils.objectIdentifier], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createIfStatement( + errorIdentifier, + f.createReturnStatement(errorIdentifier), + ), + ]), + ), + f.createReturnStatement(f.createNull()), + ]), + ); + }); +} + +export function visitShortCircuit(visitorContext: VisitorContext): string { + return VisitorUtils.setFunctionIfNotExists( + "shortCircuit", + visitorContext, + () => { + return VisitorUtils.createAcceptingFunction("shortCircuit"); + }, + ); +} diff --git a/packages/transformers/src/validateArgs/visitor-type-name.ts b/packages/transformers/src/validateArgs/visitor-type-name.ts new file mode 100644 index 000000000000..6f6d72696147 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-type-name.ts @@ -0,0 +1,233 @@ +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import type { VisitorContext } from "./visitor-context"; +import * as VisitorIndexedAccess from "./visitor-indexed-access"; +import * as VisitorKeyof from "./visitor-keyof"; +import * as VisitorUtils from "./visitor-utils"; +import { checkIsDateClass } from "./visitor-utils"; + +interface TypeCheckNameMode { + type: "type-check"; +} + +interface KeyofNameMode { + type: "keyof"; +} + +interface IndexedAccessNameMode { + type: "indexed-access"; + indexType: ts.Type; +} + +type NameMode = TypeCheckNameMode | KeyofNameMode | IndexedAccessNameMode; + +function visitTupleObjectType( + type: ts.TupleType, + visitorContext: VisitorContext, + mode: NameMode, +) { + if (type.typeArguments === undefined) { + return "st_et"; + } + const itemNames = type.typeArguments.map((type) => + visitType(type, visitorContext, mode), + ); + return `st_${itemNames.join("_")}_et`; +} + +function visitArrayObjectType( + type: ts.ObjectType, + visitorContext: VisitorContext, + mode: NameMode, +) { + const numberIndexType = visitorContext.checker.getIndexTypeOfType( + type, + ts.IndexKind.Number, + ); + if (numberIndexType === undefined) { + throw new Error( + "Expected array ObjectType to have a number index type.", + ); + } + const numberIndexName = visitType(numberIndexType, visitorContext, mode); + return `sa_${numberIndexName}_ea`; +} + +function getTypeIndexById(type: ts.Type, { typeIdMap }: VisitorContext) { + const id = (type as unknown as { id: string | number }).id.toString(); + let index = typeIdMap.get(id); + if (index === undefined) { + index = typeIdMap.size.toString(); + typeIdMap.set(id, index); + } + return index; +} + +function visitRegularObjectType(type: ts.Type, visitorContext: VisitorContext) { + const index = getTypeIndexById(type, visitorContext); + return `_${index}`; +} + +function visitTypeReference( + type: ts.TypeReference, + visitorContext: VisitorContext, + mode: NameMode, +) { + const mapping: Map = VisitorUtils.getTypeReferenceMapping( + type, + visitorContext, + ); + const previousTypeReference = visitorContext.previousTypeReference; + visitorContext.typeMapperStack.push(mapping); + visitorContext.previousTypeReference = type; + const result = visitType(type.target, visitorContext, mode); + visitorContext.previousTypeReference = previousTypeReference; + visitorContext.typeMapperStack.pop(); + return result; +} + +function visitTypeParameter( + type: ts.Type, + visitorContext: VisitorContext, + mode: NameMode, +) { + const mappedType = VisitorUtils.getResolvedTypeParameter( + type, + visitorContext, + ); + if (mappedType === undefined) { + throw new Error("Unbound type parameter, missing type node."); + } + return visitType(mappedType, visitorContext, mode); +} + +function visitObjectType( + type: ts.ObjectType, + visitorContext: VisitorContext, + mode: NameMode, +) { + if (tsutils.isTupleType(type)) { + return visitTupleObjectType(type, visitorContext, mode); + } else if ( + visitorContext.checker.getIndexTypeOfType(type, ts.IndexKind.Number) + ) { + return visitArrayObjectType(type, visitorContext, mode); + } else if (checkIsDateClass(type)) { + return "_date"; + } else { + return visitRegularObjectType(type, visitorContext); + } +} + +function visitUnionOrIntersectionType( + type: ts.UnionOrIntersectionType, + visitorContext: VisitorContext, + mode: NameMode, +) { + const names = type.types.map((type) => + visitType(type, visitorContext, mode), + ); + if (tsutils.isIntersectionType(type)) { + return `si_${names.join("_")}_ei`; + } else { + return `su_${names.join("_")}_eu`; + } +} + +function visitIndexType(type: ts.Type, visitorContext: VisitorContext) { + const indexedType = (type as { type?: ts.Type }).type; + if (indexedType === undefined) { + throw new Error("Could not get indexed type of index type."); + } + return VisitorKeyof.visitType(indexedType, visitorContext); +} + +function visitIndexedAccessType( + type: ts.IndexedAccessType, + visitorContext: VisitorContext, +) { + return VisitorIndexedAccess.visitType( + type.objectType, + type.indexType, + visitorContext, + ); +} + +export function visitType( + type: ts.Type, + visitorContext: VisitorContext, + mode: NameMode, +): string { + let name: string; + const index = getTypeIndexById(type, visitorContext); + if ((ts.TypeFlags.Any & type.flags) !== 0) { + name = VisitorUtils.getAnyFunction(visitorContext); + } else if ((ts.TypeFlags.Unknown & type.flags) !== 0) { + name = VisitorUtils.getUnknownFunction(visitorContext); + } else if ((ts.TypeFlags.Never & type.flags) !== 0) { + name = VisitorUtils.getNeverFunction(visitorContext); + } else if ((ts.TypeFlags.Null & type.flags) !== 0) { + name = VisitorUtils.getNullFunction(visitorContext); + } else if ((ts.TypeFlags.Undefined & type.flags) !== 0) { + name = VisitorUtils.getUndefinedFunction(visitorContext); + } else if ((ts.TypeFlags.Number & type.flags) !== 0) { + name = VisitorUtils.getNumberFunction(visitorContext); + } else if (VisitorUtils.isBigIntType(type)) { + name = VisitorUtils.getBigIntFunction(visitorContext); + } else if ((ts.TypeFlags.Boolean & type.flags) !== 0) { + name = VisitorUtils.getBooleanFunction(visitorContext); + } else if ((ts.TypeFlags.String & type.flags) !== 0) { + name = VisitorUtils.getStringFunction(visitorContext); + } else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) { + name = `_${index}`; + } else if ( + tsutils.isTypeReference(type) && + visitorContext.previousTypeReference !== type + ) { + name = visitTypeReference(type, visitorContext, mode); + } else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) { + name = visitTypeParameter(type, visitorContext, mode); + } else if (tsutils.isObjectType(type)) { + name = visitObjectType(type, visitorContext, mode); + } else if (tsutils.isLiteralType(type)) { + name = `_${index}`; + } else if (tsutils.isUnionOrIntersectionType(type)) { + name = visitUnionOrIntersectionType(type, visitorContext, mode); + } else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) { + name = `_${index}`; + } else if ((ts.TypeFlags.Index & type.flags) !== 0) { + name = visitIndexType(type, visitorContext); + } else if (tsutils.isIndexedAccessType(type)) { + name = visitIndexedAccessType(type, visitorContext); + } else if ((ts.TypeFlags.TemplateLiteral & type.flags) !== 0) { + name = `_${index}`; + } else { + throw new Error( + `Could not generate type-check; unsupported type with flags: ${type.flags}`, + ); + } + if (mode.type === "keyof") { + name += "_keyof"; + } + if (mode.type === "indexed-access") { + const indexTypeName = visitType(mode.indexType, visitorContext, { + type: "type-check", + }); + name += `_ia__${indexTypeName}`; + } + if (tsutils.isTypeReference(type) && type.typeArguments !== undefined) { + for (const typeArgument of type.typeArguments) { + const resolvedType = + VisitorUtils.getResolvedTypeParameter( + typeArgument, + visitorContext, + ) || typeArgument; + const resolvedTypeIndex = getTypeIndexById( + resolvedType, + visitorContext, + ); + name += `_${resolvedTypeIndex}`; + } + } + return name; +} diff --git a/packages/transformers/src/validateArgs/visitor-utils.ts b/packages/transformers/src/validateArgs/visitor-utils.ts new file mode 100644 index 000000000000..24e5ff30b4e6 --- /dev/null +++ b/packages/transformers/src/validateArgs/visitor-utils.ts @@ -0,0 +1,1011 @@ +import * as fs from "fs"; +import * as tsutils from "tsutils/typeguard/3.0"; +import ts from "typescript"; +import type { Reason } from "./reason"; +import type { PartialVisitorContext, VisitorContext } from "./visitor-context"; + +/** + * a pair of {@link ts.TemplateLiteralType.texts} and the `intrinsicName`s for {@link ts.TemplateLiteralType.types}, + * @see https://github.com/microsoft/TypeScript/pull/40336 + */ +export type TemplateLiteralPair = [ + string, + "string" | "number" | "bigint" | "any" | "undefined" | "null" | undefined, +]; + +export const objectIdentifier = ts.createIdentifier("$o"); +export const pathIdentifier = ts.createIdentifier("path"); +const keyIdentifier = ts.createIdentifier("key"); + +export function checkIsClass( + type: ts.ObjectType, + visitorContext: VisitorContext, +): boolean { + // Hacky: using internal TypeScript API. + if ( + "isArrayType" in visitorContext.checker && + (visitorContext.checker as any).isArrayType(type) + ) { + return false; + } + if ( + "isArrayLikeType" in visitorContext.checker && + (visitorContext.checker as any).isArrayLikeType(type) + ) { + return false; + } + + let hasConstructSignatures = false; + if ( + type.symbol !== undefined && + type.symbol.valueDeclaration !== undefined && + ts.isVariableDeclaration(type.symbol.valueDeclaration) && + type.symbol.valueDeclaration.type + ) { + const variableDeclarationType = + visitorContext.checker.getTypeAtLocation( + type.symbol.valueDeclaration.type, + ); + const constructSignatures = + variableDeclarationType.getConstructSignatures(); + hasConstructSignatures = constructSignatures.length >= 1; + } + + return type.isClass() || hasConstructSignatures; +} + +export function checkIsDateClass(type: ts.ObjectType): boolean { + return ( + type.symbol !== undefined && + type.symbol.valueDeclaration !== undefined && + type.symbol.escapedName === "Date" && + (ts.getCombinedModifierFlags(type.symbol.valueDeclaration) & + ts.ModifierFlags.Ambient) !== + 0 + ); +} + +export function setFunctionIfNotExists( + name: string, + visitorContext: VisitorContext, + factory: () => ts.FunctionDeclaration, +): string { + if (!visitorContext.functionNames.has(name)) { + visitorContext.functionNames.add(name); + visitorContext.functionMap.set(name, factory()); + } + return name; +} + +interface PropertyInfo { + name: string; + type: ts.Type | undefined; // undefined iff isMethod===true + isMethod: boolean; + isFunction: boolean; + isSymbol: boolean; + optional: boolean; +} + +export function getPropertyInfo( + parentType: ts.Type, + symbol: ts.Symbol, + visitorContext: VisitorContext, +): PropertyInfo { + const name: string | undefined = symbol.name; + if (name === undefined) { + throw new Error("Missing name in property symbol."); + } + + let propertyType: ts.Type | undefined = undefined; + let isMethod: boolean | undefined = undefined; + let isFunction: boolean | undefined = undefined; + let optional: boolean | undefined = undefined; + + if ("valueDeclaration" in symbol && symbol.valueDeclaration) { + // Attempt to get it from 'valueDeclaration' + + const valueDeclaration = symbol.valueDeclaration; + if ( + !ts.isPropertySignature(valueDeclaration) && + !ts.isMethodSignature(valueDeclaration) + ) { + throw new Error( + `Unsupported declaration kind: ${valueDeclaration.kind}`, + ); + } + isMethod = ts.isMethodSignature(valueDeclaration); + isFunction = + valueDeclaration.type !== undefined && + ts.isFunctionTypeNode(valueDeclaration.type); + if (valueDeclaration.type === undefined) { + if (!isMethod) { + throw new Error("Found property without type."); + } + } else { + propertyType = visitorContext.checker.getTypeFromTypeNode( + valueDeclaration.type, + ); + } + optional = !!valueDeclaration.questionToken; + } else if ("type" in symbol) { + // Attempt to get it from 'type' + + propertyType = (symbol as { type?: ts.Type }).type; + isMethod = false; + isFunction = false; + optional = (symbol.flags & ts.SymbolFlags.Optional) !== 0; + } else if ("getTypeOfPropertyOfType" in visitorContext.checker) { + // Attempt to get it from 'visitorContext.checker.getTypeOfPropertyOfType' + + propertyType = ( + visitorContext.checker as unknown as { + getTypeOfPropertyOfType: ( + type: ts.Type, + name: string, + ) => ts.Type | undefined; + } + ).getTypeOfPropertyOfType(parentType, name); + isMethod = false; + isFunction = false; + optional = (symbol.flags & ts.SymbolFlags.Optional) !== 0; + } + + if ( + optional !== undefined && + isMethod !== undefined && + isFunction !== undefined + ) { + return { + name, + type: propertyType, + isMethod, + isFunction, + isSymbol: name.startsWith("__@"), + optional, + }; + } + + throw new Error("Expected a valueDeclaration or a property type."); +} + +export function getTypeAliasMapping( + type: ts.TypeReference, +): Map { + const mapping: Map = new Map(); + if ( + type.aliasTypeArguments !== undefined && + type.target.aliasTypeArguments !== undefined + ) { + const typeParameters = type.target.aliasTypeArguments; + const typeArguments = type.aliasTypeArguments; + for (let i = 0; i < typeParameters.length; i++) { + if (typeParameters[i] !== typeArguments[i]) { + mapping.set(typeParameters[i], typeArguments[i]); + } + } + } + return mapping; +} + +export function getTypeReferenceMapping( + type: ts.TypeReference, + visitorContext: VisitorContext, +): Map { + const mapping: Map = new Map(); + (function checkBaseTypes(type: ts.TypeReference) { + if (tsutils.isInterfaceType(type.target)) { + const baseTypes = visitorContext.checker.getBaseTypes(type.target); + for (const baseType of baseTypes) { + if ( + baseType.aliasTypeArguments && + visitorContext.previousTypeReference !== baseType && + (baseType as ts.TypeReference).target + ) { + const typeReference = baseType as ts.TypeReference; + if ( + typeReference.aliasTypeArguments !== undefined && + typeReference.target.aliasTypeArguments !== undefined + ) { + const typeParameters = + typeReference.target.aliasTypeArguments; + const typeArguments = typeReference.aliasTypeArguments; + for (let i = 0; i < typeParameters.length; i++) { + if (typeParameters[i] !== typeArguments[i]) { + mapping.set( + typeParameters[i], + typeArguments[i], + ); + } + } + } + } + + if ( + tsutils.isTypeReference(baseType) && + baseType.target.typeParameters !== undefined && + baseType.typeArguments !== undefined + ) { + const typeParameters = baseType.target.typeParameters; + const typeArguments = baseType.typeArguments; + for (let i = 0; i < typeParameters.length; i++) { + if (typeParameters[i] !== typeArguments[i]) { + mapping.set(typeParameters[i], typeArguments[i]); + } + } + checkBaseTypes(baseType); + } + } + } + })(type); + if ( + type.target.typeParameters !== undefined && + type.typeArguments !== undefined + ) { + const typeParameters = type.target.typeParameters; + const typeArguments = type.typeArguments; + for (let i = 0; i < typeParameters.length; i++) { + if (typeParameters[i] !== typeArguments[i]) { + mapping.set(typeParameters[i], typeArguments[i]); + } + } + } + return mapping; +} + +export function getResolvedTypeParameter( + type: ts.Type, + visitorContext: VisitorContext, +): ts.Type | undefined { + let mappedType: ts.Type | undefined; + for (let i = visitorContext.typeMapperStack.length - 1; i >= 0; i--) { + mappedType = visitorContext.typeMapperStack[i].get(type); + if (mappedType !== undefined) { + break; + } + } + return mappedType || type.getDefault(); +} + +export function getFunctionFunction(visitorContext: VisitorContext): string { + const name = "_function"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + ts.createTypeOf(objectIdentifier), + ts.createStringLiteral("function"), + ), + { type: "function" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getStringFunction(visitorContext: VisitorContext): string { + const name = "_string"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + ts.createTypeOf(objectIdentifier), + ts.createStringLiteral("string"), + ), + { type: "string" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getBooleanFunction(visitorContext: VisitorContext): string { + const name = "_boolean"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + ts.createTypeOf(objectIdentifier), + ts.createStringLiteral("boolean"), + ), + { type: "boolean" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getBigIntFunction(visitorContext: VisitorContext): string { + const name = "_bigint"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + ts.createTypeOf(objectIdentifier), + ts.createStringLiteral("bigint"), + ), + { type: "big-int" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getNumberFunction(visitorContext: VisitorContext): string { + const name = "_number"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + ts.createTypeOf(objectIdentifier), + ts.createStringLiteral("number"), + ), + { type: "number" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getUndefinedFunction(visitorContext: VisitorContext): string { + const name = "_undefined"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAssertionFunction( + ts.createStrictInequality( + objectIdentifier, + ts.createIdentifier("undefined"), + ), + { type: "undefined" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getNullFunction(visitorContext: VisitorContext): string { + const name = "_null"; + return setFunctionIfNotExists(name, visitorContext, () => { + const strictNullChecks = + visitorContext.compilerOptions.strictNullChecks !== undefined + ? visitorContext.compilerOptions.strictNullChecks + : !!visitorContext.compilerOptions.strict; + + if (!strictNullChecks) { + return createAcceptingFunction(name); + } + + return createAssertionFunction( + ts.createStrictInequality(objectIdentifier, ts.createNull()), + { type: "null" }, + name, + visitorContext, + createStrictNullCheckStatement(objectIdentifier, visitorContext), + ); + }); +} + +export function getNeverFunction(visitorContext: VisitorContext): string { + const name = "_never"; + return setFunctionIfNotExists(name, visitorContext, () => { + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + name, + undefined, + [ + ts.createParameter( + undefined, + undefined, + undefined, + objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + ts.createBlock( + [ + ts.createReturn( + createErrorObject({ type: "never" }, visitorContext), + ), + ], + true, + ), + ); + }); +} + +export function getUnknownFunction(visitorContext: VisitorContext): string { + const name = "_unknown"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAcceptingFunction(name); + }); +} + +export function getAnyFunction(visitorContext: VisitorContext): string { + const name = "_any"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAcceptingFunction(name); + }); +} + +export function getIgnoredTypeFunction(visitorContext: VisitorContext): string { + const name = "_ignore"; + return setFunctionIfNotExists(name, visitorContext, () => { + return createAcceptingFunction(name); + }); +} + +export function createBinaries( + expressions: ts.Expression[], + operator: ts.BinaryOperator, + baseExpression?: ts.Expression, +): ts.Expression { + if (expressions.length >= 1 || baseExpression === undefined) { + return expressions.reduce((previous, expression) => + ts.createBinary(previous, operator, expression), + ); + } else { + return baseExpression; + } +} + +export function createAcceptingFunction( + functionName: string, +): ts.FunctionDeclaration { + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + functionName, + undefined, + [], + undefined, + ts.createBlock([ts.createReturn(ts.createNull())], true), + ); +} + +export function createConjunctionFunction( + functionNames: string[], + functionName: string, + extraStatements?: ts.Statement[], +): ts.FunctionDeclaration { + const conditionsIdentifier = ts.createIdentifier("conditions"); + const conditionIdentifier = ts.createIdentifier("condition"); + const errorIdentifier = ts.createIdentifier("error"); + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + functionName, + undefined, + [ + ts.createParameter( + undefined, + undefined, + undefined, + objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + ts.createBlock( + [ + ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + conditionsIdentifier, + undefined, + ts.createArrayLiteral( + functionNames.map((functionName) => + ts.createIdentifier(functionName), + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ts.createForOf( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + conditionIdentifier, + undefined, + undefined, + ), + ], + ts.NodeFlags.Const, + ), + conditionsIdentifier, + ts.createBlock( + [ + ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + errorIdentifier, + undefined, + ts.createCall( + conditionIdentifier, + undefined, + [objectIdentifier], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ts.createIf( + errorIdentifier, + ts.createReturn(errorIdentifier), + ), + ], + true, + ), + ), + ...(extraStatements || []), + ts.createReturn(ts.createNull()), + ], + true, + ), + ); +} + +export function createDisjunctionFunction( + functionNames: string[], + functionName: string, + visitorContext: VisitorContext, +): ts.FunctionDeclaration { + if (functionNames.length === 2) { + const nullTypeCheckFunction = getNullFunction(visitorContext); + const nullIndex = functionNames.indexOf(nullTypeCheckFunction); + if (nullIndex > -1) { + return createNullableTypeCheck( + functionNames[1 - nullIndex], + functionName, + ); + } + } + + const conditionsIdentifier = ts.createIdentifier("conditions"); + const conditionIdentifier = ts.createIdentifier("condition"); + const errorIdentifier = ts.createIdentifier("error"); + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + functionName, + undefined, + [ + ts.createParameter( + undefined, + undefined, + undefined, + objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + ts.createBlock( + [ + ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + conditionsIdentifier, + undefined, + ts.createArrayLiteral( + functionNames.map((functionName) => + ts.createIdentifier(functionName), + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ts.createForOf( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + conditionIdentifier, + undefined, + undefined, + ), + ], + ts.NodeFlags.Const, + ), + conditionsIdentifier, + ts.createBlock( + [ + ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + errorIdentifier, + undefined, + ts.createCall( + conditionIdentifier, + undefined, + [objectIdentifier], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ts.createIf( + ts.createLogicalNot(errorIdentifier), + ts.createReturn(ts.createNull()), + ), + ], + true, + ), + ), + ts.createReturn( + createErrorObject({ type: "union" }, visitorContext), + ), + ], + true, + ), + ); +} + +function createNullableTypeCheck( + typeCheckFunction: string, + functionName: string, +) { + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + functionName, + undefined, + [ + ts.createParameter( + undefined, + undefined, + undefined, + objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + ts.createBlock( + [ + ts.createIf( + ts.createStrictEquality(objectIdentifier, ts.createNull()), + ts.createReturn(ts.createNull()), + ts.createReturn( + ts.createCall( + ts.createIdentifier(typeCheckFunction), + undefined, + [objectIdentifier], + ), + ), + ), + ], + true, + ), + ); +} + +export function createStrictNullCheckStatement( + identifier: ts.Identifier, + visitorContext: VisitorContext, +): ts.Statement { + if (visitorContext.compilerOptions.strictNullChecks !== false) { + return ts.createEmptyStatement(); + } else { + return ts.createIf( + ts.createBinary( + ts.createStrictEquality(identifier, ts.createNull()), + ts.SyntaxKind.BarBarToken, + ts.createStrictEquality( + identifier, + ts.createIdentifier("undefined"), + ), + ), + ts.createReturn(ts.createNull()), + ); + } +} + +export function createAssertionFunction( + failureCondition: ts.Expression, + expected: Reason, + functionName: string, + visitorContext: VisitorContext, + ...otherStatements: ts.Statement[] +): ts.FunctionDeclaration { + const f = visitorContext.factory; + return f.createFunctionDeclaration( + undefined, + undefined, + undefined, + functionName, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + undefined, + objectIdentifier, + undefined, + undefined, + undefined, + ), + ], + undefined, + f.createBlock( + [ + ...otherStatements.filter((o) => !ts.isEmptyStatement(o)), + f.createReturnStatement( + f.createConditionalExpression( + failureCondition, + f.createToken(ts.SyntaxKind.QuestionToken), + createErrorObject(expected, visitorContext), + f.createToken(ts.SyntaxKind.ColonToken), + f.createNull(), + ), + ), + ], + true, + ), + ); +} + +export function createSuperfluousPropertiesLoop( + propertyNames: string[], + visitorContext: VisitorContext, +): ts.Statement { + return ts.createForOf( + undefined, + ts.createVariableDeclarationList( + [ts.createVariableDeclaration(keyIdentifier, undefined, undefined)], + ts.NodeFlags.Const, + ), + ts.createCall( + ts.createPropertyAccess(ts.createIdentifier("Object"), "keys"), + undefined, + [objectIdentifier], + ), + ts.createBlock( + [ + ts.createIf( + createBinaries( + propertyNames.map((propertyName) => + ts.createStrictInequality( + keyIdentifier, + ts.createStringLiteral(propertyName), + ), + ), + ts.SyntaxKind.AmpersandAmpersandToken, + ts.createTrue(), + ), + ts.createReturn( + createErrorObject( + { type: "superfluous-property" }, + visitorContext, + ), + ), + ), + ], + true, + ), + ); +} + +export function isBigIntType(type: ts.Type): boolean { + if ("BigInt" in ts.TypeFlags) { + return !!((ts.TypeFlags as any).BigInt & type.flags); + } else { + return false; + } +} + +function createAssertionString(reason: string | ts.Expression): ts.Expression { + if (typeof reason === "string") { + return createBinaries( + [ + ts.createStringLiteral("validation failed at "), + ts.createCall( + ts.createPropertyAccess(pathIdentifier, "join"), + undefined, + [ts.createStringLiteral(".")], + ), + ts.createStringLiteral(`: ${reason}`), + ], + ts.SyntaxKind.PlusToken, + ); + } else { + return createBinaries( + [ + ts.createStringLiteral("validation failed at "), + ts.createCall( + ts.createPropertyAccess(pathIdentifier, "join"), + undefined, + [ts.createStringLiteral(".")], + ), + ts.createStringLiteral(`: `), + reason, + ], + ts.SyntaxKind.PlusToken, + ); + } +} + +export function createErrorObject( + reason: Reason, + visitorContext: VisitorContext, +): ts.Expression { + if (visitorContext.options.emitDetailedErrors === false) { + return ts.createObjectLiteral([]); + } + return ts.createObjectLiteral([ + ts.createPropertyAssignment("message", createErrorMessage(reason)), + ts.createPropertyAssignment( + "path", + ts.createCall( + ts.createPropertyAccess(pathIdentifier, "slice"), + undefined, + undefined, + ), + ), + ts.createPropertyAssignment( + "reason", + serializeObjectToExpression(reason), + ), + ]); +} + +function serializeObjectToExpression(object: unknown): ts.Expression { + if (typeof object === "string") { + return ts.createStringLiteral(object); + } else if (typeof object === "number") { + return ts.createNumericLiteral(object.toString()); + } else if (typeof object === "boolean") { + return object ? ts.createTrue() : ts.createFalse(); + } else if (typeof object === "bigint") { + return ts.createBigIntLiteral(object.toString()); + } else if (typeof object === "undefined") { + return ts.createIdentifier("undefined"); + } else if (typeof object === "object") { + if (object === null) { + return ts.createNull(); + } else if (Array.isArray(object)) { + return ts.createArrayLiteral( + object.map((item) => serializeObjectToExpression(item)), + ); + } else { + return ts.createObjectLiteral( + Object.keys(object).map((key) => { + const value = (object as { [Key: string]: unknown })[key]; + return ts.createPropertyAssignment( + key, + serializeObjectToExpression(value), + ); + }), + ); + } + } + throw new Error("Cannot serialize object to expression."); +} + +function createErrorMessage(reason: Reason): ts.Expression { + switch (reason.type) { + case "tuple": + return createAssertionString( + `expected an array with length ${reason.minLength}-${reason.maxLength}`, + ); + case "array": + return createAssertionString("expected an array"); + case "object": + return createAssertionString("expected an object"); + case "missing-property": + return createAssertionString( + `expected '${reason.property}' in object`, + ); + case "superfluous-property": + return createAssertionString( + createBinaries( + [ + ts.createStringLiteral(`superfluous property '`), + keyIdentifier, + ts.createStringLiteral(`' in object`), + ], + ts.SyntaxKind.PlusToken, + ), + ); + case "never": + return createAssertionString("type is never"); + case "union": + return createAssertionString("there are no valid alternatives"); + case "string": + return createAssertionString("expected a string"); + case "boolean": + return createAssertionString("expected a boolean"); + case "big-int": + return createAssertionString("expected a bigint"); + case "number": + return createAssertionString("expected a number"); + case "undefined": + return createAssertionString("expected undefined"); + case "null": + return createAssertionString("expected null"); + case "object-keyof": + return createAssertionString( + `expected ${reason.properties + .map((property) => `'${property}'`) + .join("|")}`, + ); + case "string-literal": + return createAssertionString(`expected string '${reason.value}'`); + case "number-literal": + return createAssertionString(`expected number '${reason.value}'`); + case "boolean-literal": + return createAssertionString( + `expected ${reason.value ? "true" : "false"}`, + ); + case "non-primitive": + return createAssertionString("expected a non-primitive"); + case "date": + return createAssertionString("expected a Date"); + case "function": + return createAssertionString("expected a function"); + case "template-literal": + return createAssertionString( + `expected \`${reason.value + .map( + ([text, type]) => + text + + (typeof type === "undefined" + ? "" + : "${" + type + "}"), + ) + .join("")}\``, + ); + } + throw new Error("Not implemented"); +} + +export function getIntrinsicName(type: ts.Type): string | undefined { + // Using internal TypeScript API, hacky. + return (type as { intrinsicName?: string }).intrinsicName; +} + +export function getCanonicalPath( + path: string, + context: PartialVisitorContext, +): string { + if (!context.canonicalPaths.has(path)) { + context.canonicalPaths.set(path, fs.realpathSync(path)); + } + return context.canonicalPaths.get(path)!; +} + +export function createBlock( + factory: ts.NodeFactory, + statements: ts.Statement[], +): ts.Block { + return factory.createBlock( + statements.filter((s) => !ts.isEmptyStatement(s)), + true, + ); +} diff --git a/packages/transformers/test/.gitignore b/packages/transformers/test/.gitignore new file mode 100644 index 000000000000..a6c7c2852d06 --- /dev/null +++ b/packages/transformers/test/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/packages/transformers/test/test1.ts b/packages/transformers/test/test1.ts new file mode 100644 index 000000000000..c86c5ce19a9e --- /dev/null +++ b/packages/transformers/test/test1.ts @@ -0,0 +1,53 @@ +import { validateArgs } from "@zwave-js/transformers"; +import assert from "assert"; + +type Foo = { + a: 1; + b: 2; +}; + +type Bar = { + c: 3; +}; + +class Test { + @validateArgs() + foo(arg1: number, arg2: Foo = { a: 1, b: 2 }, arg3?: Foo & Bar): void { + arg1; + arg2; + arg3; + return void 0; + } +} + +const test = new Test(); +// These should not throw +test.foo(1, { a: 1, b: 2 }, { a: 1, b: 2, c: 3 }); +test.foo(1, { a: 1, b: 2 }, undefined); + +// These should throw +assert.throws( + // @ts-expect-error + () => test.foo(1, { a: 1, b: 2 }, { a: 1, b: 2 }), + /arg3 has the wrong type/, +); +assert.throws( + // @ts-expect-error + () => test.foo(1, { a: 1, b: "2" }, { a: 1, b: 2, c: 3 }), + /arg2 is not a \(optional\) Foo/, +); +assert.throws( + // @ts-expect-error + () => test.foo(true, { a: 1, b: 2 }, undefined), + /arg1 is not a number/, +); +assert.throws( + // @ts-expect-error + () => test.foo(undefined, { a: 1, b: 2 }, undefined), + /arg1 is not a number/, +); +assert.throws( + // @ts-expect-error + () => test.foo(2, 2, undefined), + /arg2 is not a \(optional\) Foo/, +); diff --git a/packages/transformers/tsconfig.build.json b/packages/transformers/tsconfig.build.json new file mode 100644 index 000000000000..93be75208403 --- /dev/null +++ b/packages/transformers/tsconfig.build.json @@ -0,0 +1,15 @@ +// tsconfig for building - only applies to the src directory +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "references": [ + { + "path": "../maintenance/tsconfig.build.json" + } + ], + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/transformers/tsconfig.json b/packages/transformers/tsconfig.json new file mode 100644 index 000000000000..06c1ea4adc83 --- /dev/null +++ b/packages/transformers/tsconfig.json @@ -0,0 +1,19 @@ +// tsconfig for IntelliSense - active in all files in the current package +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["ES2020"], + "newLine": "lf", + "experimentalDecorators": true, + "paths": { + "@zwave-js/transformers": ["./src"] + } + }, + "references": [ + { + "path": "../maintenance/tsconfig.build.json" + } + ], + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["build/**", "node_modules/**"] +} diff --git a/packages/transformers/tsconfig.test.json b/packages/transformers/tsconfig.test.json new file mode 100644 index 000000000000..9e4c21031204 --- /dev/null +++ b/packages/transformers/tsconfig.test.json @@ -0,0 +1,24 @@ +// tsconfig for building - only applies to the src directory +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "rootDir": "test", + "outDir": "test", + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "plugins": [{ "transform": "./build" }], + "incremental": false, + "experimentalDecorators": true + }, + "references": [ + { + "path": "../maintenance/tsconfig.build.json" + }, + { + "path": "./tsconfig.build.json" + } + ], + "include": ["test/**/*.ts"] +} diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index 4b4922b55954..bc9566f48044 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -83,6 +83,7 @@ "@xstate/test": "^0.5.1", "@zwave-js/maintenance": "workspace:*", "@zwave-js/testing": "workspace:*", + "@zwave-js/transformers": "workspace:*", "esbuild": "0.14.27", "esbuild-register": "^3.3.2", "jest-extended": "^2.0.0", diff --git a/packages/zwave-js/tsconfig.build.json b/packages/zwave-js/tsconfig.build.json index a8b9f0d61fa1..99d9e641fd79 100644 --- a/packages/zwave-js/tsconfig.build.json +++ b/packages/zwave-js/tsconfig.build.json @@ -20,6 +20,9 @@ }, { "path": "../shared/tsconfig.build.json" + }, + { + "path": "../transformers/tsconfig.build.json" } ], "include": ["src/**/*.ts"], diff --git a/packages/zwave-js/tsconfig.json b/packages/zwave-js/tsconfig.json index f3c96ba441db..3392831c1ab4 100644 --- a/packages/zwave-js/tsconfig.json +++ b/packages/zwave-js/tsconfig.json @@ -1,7 +1,12 @@ // tsconfig for IntelliSense - active in all files in the current package { "extends": "../../tsconfig.json", - "compilerOptions": {}, + "compilerOptions": { + "plugins": [ + { "transform": "@zwave-js/transformers" }, + { "transform": "ts-nameof", "type": "raw" } + ] + }, "references": [ { "path": "../config/tsconfig.build.json" @@ -23,6 +28,9 @@ }, { "path": "../testing/tsconfig.build.json" + }, + { + "path": "../transformers/tsconfig.build.json" } ], "include": ["maintenance/**/*.ts", "src/**/*.ts"], diff --git a/tsconfig.json b/tsconfig.json index e9ff43227119..a75fb207645f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ "watch": false, // true breaks CI scripts "pretty": true, - "types": ["node", "jest", "jest-extended"], + "types": ["node", "jest", "jest-extended", "ts-nameof"], "noErrorTruncation": true }, "include": [ diff --git a/yarn.lock b/yarn.lock index 895ed7b0228a..85f7d1a551ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3019,6 +3019,32 @@ __metadata: languageName: node linkType: hard +"@ts-nameof/common@npm:^4.2.0": + version: 4.2.1 + resolution: "@ts-nameof/common@npm:4.2.1" + checksum: fd107fbbc1c46d6c918858786536275ed330d85d91a9a4afe33c29e269d747c626438e78c2786a6104e02f8609ff18ffacedfa16971cc44fef86c89775bd91bf + languageName: node + linkType: hard + +"@ts-nameof/transforms-common@npm:^4.2.1": + version: 4.2.1 + resolution: "@ts-nameof/transforms-common@npm:4.2.1" + dependencies: + "@ts-nameof/common": ^4.2.0 + checksum: 434f9e632cd932d0b152018074731d2cd59b9b053a6213b59b8393befeb7fb5c94d5c3df9c886a790d690793d4f02ecefb135a77f4e2462a74cf33f3c37dfe2a + languageName: node + linkType: hard + +"@ts-nameof/transforms-ts@npm:^4.2.1": + version: 4.2.1 + resolution: "@ts-nameof/transforms-ts@npm:4.2.1" + dependencies: + "@ts-nameof/common": ^4.2.0 + "@ts-nameof/transforms-common": ^4.2.1 + checksum: 9c1d3f5dbed313d471bfd2d1ba419ce4435aa4313a592afd84cfb8e1574b4b5b9bd201ede81a13cfc8695322fac4fed0162e417b561dcd012a81ec08ecd3e18a + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.8 resolution: "@tsconfig/node10@npm:1.0.8" @@ -3293,6 +3319,13 @@ __metadata: languageName: node linkType: hard +"@types/ts-nameof@npm:^4.2.1": + version: 4.2.1 + resolution: "@types/ts-nameof@npm:4.2.1" + checksum: 586c4bb8269193671b61c327ca7032b9581b67a1faf3b78d908c405aeae40324621bf9c1090da2bd00685569e30c52fcc7eb55f03d0112b38541effb6372c178 + languageName: node + linkType: hard + "@types/xml2json@npm:^0.11.4": version: 0.11.4 resolution: "@types/xml2json@npm:0.11.4" @@ -3613,6 +3646,7 @@ __metadata: "@types/node": ^15.12.5 "@types/semver": ^7.3.9 "@types/source-map-support": ^0 + "@types/ts-nameof": ^4.2.1 "@typescript-eslint/eslint-plugin": ^5.13.0 "@typescript-eslint/parser": ^5.13.0 "@zwave-js/config": "workspace:*" @@ -3639,6 +3673,8 @@ __metadata: reflect-metadata: ^0.1.13 semver: ^7.3.5 source-map-support: ^0.5.21 + ts-nameof: ^5.0.0 + ts-patch: ^2.0.1 typescript: 4.6.2 zwave-js: "workspace:*" languageName: unknown @@ -3702,6 +3738,15 @@ __metadata: languageName: unknown linkType: soft +"@zwave-js/transformers@workspace:*, @zwave-js/transformers@workspace:packages/transformers": + version: 0.0.0-use.local + resolution: "@zwave-js/transformers@workspace:packages/transformers" + dependencies: + tsutils: ^3.21.0 + typescript: 4.6.2 + languageName: unknown + linkType: soft + "@zwave-js/winston-daily-rotate-file@npm:^4.5.6-1": version: 4.5.6-1 resolution: "@zwave-js/winston-daily-rotate-file@npm:4.5.6-1" @@ -4440,6 +4485,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^4.1.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -6223,6 +6278,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:^7.0.0, glob@npm:^7.1.6, glob@npm:^7.1.7": + version: 7.2.0 + resolution: "glob@npm:7.2.0" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 + languageName: node + linkType: hard + "glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.1.7 resolution: "glob@npm:7.1.7" @@ -6270,6 +6339,17 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^3.0.0": + version: 3.0.0 + resolution: "global-prefix@npm:3.0.0" + dependencies: + ini: ^1.3.5 + kind-of: ^6.0.2 + which: ^1.3.1 + checksum: 8a82fc1d6f22c45484a4e34656cc91bf021a03e03213b0035098d605bfc612d7141f1e14a21097e8a0413b4884afd5b260df0b6a25605ce9d722e11f1df2881d + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -6646,7 +6726,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.4": +"ini@npm:^1.3.4, ini@npm:^1.3.5": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 @@ -6674,6 +6754,13 @@ __metadata: languageName: node linkType: hard +"interpret@npm:^1.0.0": + version: 1.4.0 + resolution: "interpret@npm:1.4.0" + checksum: 2e5f51268b5941e4a17e4ef0575bc91ed0ab5f8515e3cf77486f7c14d13f3010df9c0959f37063dcc96e78d12dc6b0bb1b9e111cdfe69771f4656d2993d36155 + languageName: node + linkType: hard + "ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -6704,6 +6791,15 @@ __metadata: languageName: node linkType: hard +"is-core-module@npm:^2.8.1": + version: 2.8.1 + resolution: "is-core-module@npm:2.8.1" + dependencies: + has: ^1.0.3 + checksum: 418b7bc10768a73c41c7ef497e293719604007f88934a6ffc5f7c78702791b8528102fb4c9e56d006d69361549b3d9519440214a74aefc7e0b79e5e4411d377f + languageName: node + linkType: hard + "is-docker@npm:^2.0.0": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -7663,7 +7759,7 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.3": +"kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b @@ -8626,7 +8722,7 @@ __metadata: languageName: node linkType: hard -"path-parse@npm:^1.0.6": +"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a @@ -8907,6 +9003,15 @@ __metadata: languageName: node linkType: hard +"rechoir@npm:^0.6.2": + version: 0.6.2 + resolution: "rechoir@npm:0.6.2" + dependencies: + resolve: ^1.1.6 + checksum: fe76bf9c21875ac16e235defedd7cbd34f333c02a92546142b7911a0f7c7059d2e16f441fe6fb9ae203f459c05a31b2bcf26202896d89e390eda7514d5d2702b + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -9096,6 +9201,32 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.1.6": + version: 1.22.0 + resolution: "resolve@npm:1.22.0" + dependencies: + is-core-module: ^2.8.1 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: a2d14cc437b3a23996f8c7367eee5c7cf8149c586b07ca2ae00e96581ce59455555a1190be9aa92154785cf9f2042646c200d0e00e0bbd2b8a995a93a0ed3e4e + languageName: node + linkType: hard + +"resolve@patch:resolve@^1.1.6#~builtin": + version: 1.22.0 + resolution: "resolve@patch:resolve@npm%3A1.22.0#~builtin::version=1.22.0&hash=07638b" + dependencies: + is-core-module: ^2.8.1 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: c79ecaea36c872ee4a79e3db0d3d4160b593f2ca16e031d8283735acd01715a203607e9ded3f91f68899c2937fa0d49390cddbe0fb2852629212f3cda283f4a7 + languageName: node + linkType: hard + "resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.20.0#~builtin": version: 1.20.0 resolution: "resolve@patch:resolve@npm%3A1.20.0#~builtin::version=1.20.0&hash=07638b" @@ -9309,6 +9440,19 @@ __metadata: languageName: node linkType: hard +"shelljs@npm:^0.8.4": + version: 0.8.5 + resolution: "shelljs@npm:0.8.5" + dependencies: + glob: ^7.0.0 + interpret: ^1.0.0 + rechoir: ^0.6.2 + bin: + shjs: bin/shjs + checksum: 7babc46f732a98f4c054ec1f048b55b9149b98aa2da32f6cf9844c434b43c6251efebd6eec120937bd0999e13811ebd45efe17410edb3ca938f82f9381302748 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": version: 3.0.3 resolution: "signal-exit@npm:3.0.3" @@ -9701,6 +9845,13 @@ __metadata: languageName: node linkType: hard +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -9899,6 +10050,19 @@ __metadata: languageName: node linkType: hard +"ts-nameof@npm:^5.0.0": + version: 5.0.0 + resolution: "ts-nameof@npm:5.0.0" + dependencies: + "@ts-nameof/common": ^4.2.0 + "@ts-nameof/transforms-ts": ^4.2.1 + glob: ^7.1.6 + peerDependencies: + typescript: "*" + checksum: bbbe5aeed2375d82865e04fdbca0ebe601536ce52c9b5aca75761cdb602c8c844ee7be929984a7f7ee4beeb5a328d066f56b7da829d523436cda518c3aeb4cfb + languageName: node + linkType: hard + "ts-node@npm:^10.4.0": version: 10.4.0 resolution: "ts-node@npm:10.4.0" @@ -9935,6 +10099,25 @@ __metadata: languageName: node linkType: hard +"ts-patch@npm:^2.0.1": + version: 2.0.1 + resolution: "ts-patch@npm:2.0.1" + dependencies: + chalk: ^4.1.0 + glob: ^7.1.7 + global-prefix: ^3.0.0 + minimist: ^1.2.5 + resolve: ^1.20.0 + shelljs: ^0.8.4 + strip-ansi: ^6.0.0 + peerDependencies: + typescript: ">=4.0.0" + bin: + ts-patch: bin/cli.js + checksum: 0460cf3b79655ebac6909383af85675769dd8031464782b02b71900409f75a09df929abdfe903f1fd0c6f89afc8dfacea1cf80dfb5a527f53a7ad4c16d3ad790 + languageName: node + linkType: hard + "ts-pegjs@npm:^0.3.1": version: 0.3.1 resolution: "ts-pegjs@npm:0.3.1" @@ -10324,7 +10507,7 @@ typescript@^4.4.3: languageName: node linkType: hard -"which@npm:^1.2.14, which@npm:^1.2.9": +"which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -10597,6 +10780,7 @@ typescript@^4.4.3: "@zwave-js/serial": "workspace:*" "@zwave-js/shared": "workspace:*" "@zwave-js/testing": "workspace:*" + "@zwave-js/transformers": "workspace:*" alcalzone-shared: ^4.0.1 ansi-colors: ^4.1.1 axios: ^0.26.0