diff --git a/package.json b/package.json index 60d8d81..3aae526 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "repository": "github:rhys-vdw/ts-auto-guard", "main": "lib/index.js", "scripts": { - "test": "cross-env NODE_ENV=test && npm run lint && npm run format:check && tape -r ts-node/register tests/**/*.ts | tap-diff", + "test": "cross-env NODE_ENV=test && npm run lint && npm run format:check && tape -r ts-node/register tests/**/*.test.ts | tap-diff", "build": "tsc", "prepare": "npm run build", "lint": "eslint .", diff --git a/tests/features/adds_type_guard_import_to_source_file_and_also_exports.test.ts b/tests/features/adds_type_guard_import_to_source_file_and_also_exports.test.ts new file mode 100644 index 0000000..27e1771 --- /dev/null +++ b/tests/features/adds_type_guard_import_to_source_file_and_also_exports.test.ts @@ -0,0 +1,34 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'adds type guard import to source file and also exports', + { + // NOTE: This file is not automatically cleaned up with `formatText` after + // being modified so it requires this funky indentation to ensure that it is + // conforms to ts-morph's formatting. + 'test.ts': ` +/** @see {isEmpty} ts-auto-guard:type-guard */ +export interface Empty { } +`, + }, + { + 'test.ts': ` + import * as CustomGuardAlias from "./test.guard"; + + /** @see {isEmpty} ts-auto-guard:type-guard */ + export interface Empty {} + export { CustomGuardAlias };`, + 'test.guard.ts': ` + import { Empty } from "./test"; + + export function isEmpty(obj: unknown): obj is Empty { + const typedObj = obj as Empty + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") + ) + }`, + }, + { options: { importGuards: 'CustomGuardAlias' } } +) diff --git a/tests/features/allows_the_name_of_the_guard_file_file_to_be_specified.test.ts b/tests/features/allows_the_name_of_the_guard_file_file_to_be_specified.test.ts new file mode 100644 index 0000000..740e236 --- /dev/null +++ b/tests/features/allows_the_name_of_the_guard_file_file_to_be_specified.test.ts @@ -0,0 +1,34 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'allows the name of the guard file file to be specified', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: number, + bar: string + }`, + }, + { + 'test.ts': null, + 'test.debug.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" && + typeof typedObj["bar"] === "string" + ) + }`, + }, + { + options: { + guardFileName: 'debug', + }, + } +) diff --git a/tests/features/any_and_unknown_work_in_interesction_types.test.ts b/tests/features/any_and_unknown_work_in_interesction_types.test.ts new file mode 100644 index 0000000..16f04fb --- /dev/null +++ b/tests/features/any_and_unknown_work_in_interesction_types.test.ts @@ -0,0 +1,41 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'any and unknown work in interesction types', + { + 'test.ts': ` + type anyType = any + type unknownType = unknown + + export type AnyAndString = string & anyType + export type UnknownAndString = string & unknownType + export type AnyAndUnknownAndString = string & anyType & unknownType`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { AnyAndString, UnknownAndString, AnyAndUnknownAndString } from "./test"; + + export function isAnyAndString(obj: unknown): obj is AnyAndString { + const typedObj = obj as AnyAndString + return ( + true + ) + } + + export function isUnknownAndString(obj: unknown): obj is UnknownAndString { + const typedObj = obj as UnknownAndString + return ( + typeof typedObj === "string" + ) + } + + export function isAnyAndUnknownAndString(obj: unknown): obj is AnyAndUnknownAndString { + const typedObj = obj as AnyAndUnknownAndString + return ( + true + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/any_and_unknown_work_in_union_types.test.ts b/tests/features/any_and_unknown_work_in_union_types.test.ts new file mode 100644 index 0000000..6e4d11c --- /dev/null +++ b/tests/features/any_and_unknown_work_in_union_types.test.ts @@ -0,0 +1,41 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'any and unknown work in union types', + { + 'test.ts': ` + type anyType = any + type unknownType = unknown + + export type AnyOrString = string | anyType + export type UnknownOrString = string | unknownType + export type AnyOrUnknownOrString = string | anyType | unknownType`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { AnyOrString, UnknownOrString, AnyOrUnknownOrString } from "./test"; + + export function isAnyOrString(obj: unknown): obj is AnyOrString { + const typedObj = obj as AnyOrString + return ( + true + ) + } + + export function isUnknownOrString(obj: unknown): obj is UnknownOrString { + const typedObj = obj as UnknownOrString + return ( + true + ) + } + + export function isAnyOrUnknownOrString(obj: unknown): obj is AnyOrUnknownOrString { + const typedObj = obj as AnyOrUnknownOrString + return ( + true + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/check_if_any_callable_properties_is_a_function.test.ts b/tests/features/check_if_any_callable_properties_is_a_function.test.ts new file mode 100644 index 0000000..b58c4e9 --- /dev/null +++ b/tests/features/check_if_any_callable_properties_is_a_function.test.ts @@ -0,0 +1,40 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'Check if any callable properties is a function', + // should also emit a warning about how it is not possible to check function type at runtime. + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + test: (() => void) + // ts-auto-guard-suppress function-type + test2(someArg: number): boolean + // some other comments + test3: { + (someArg: string): number + test3Arg: number; + } + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["test"] === "function" && + typeof typedObj["test3"] === "function" && + typeof typedObj["test3"]["test3Arg"] === "number" && + typeof typedObj["test2"] === "function" + ) + } + `, + } +) diff --git a/tests/features/check_if_callable_interface_is_a_function.test.ts b/tests/features/check_if_callable_interface_is_a_function.test.ts new file mode 100644 index 0000000..043098c --- /dev/null +++ b/tests/features/check_if_callable_interface_is_a_function.test.ts @@ -0,0 +1,29 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'Check if callable interface is a function', + // should also emit a warning about how it is not possible to check function type at runtime. + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + (someArg: string): number + arg: number; + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + typeof typedObj === "function" && + typeof typedObj["arg"] === "number" + ) + } + `, + } +) diff --git a/tests/features/correctly_handles_default_export.test.ts b/tests/features/correctly_handles_default_export.test.ts new file mode 100644 index 0000000..aaf8167 --- /dev/null +++ b/tests/features/correctly_handles_default_export.test.ts @@ -0,0 +1,31 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'correctly handles default export', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + interface Foo { + foo: number, + bar: string + } + + export default Foo`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import Foo from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" && + typeof typedObj["bar"] === "string" + ) + }`, + } +) diff --git a/tests/features/deals_with_unknown_type_as_it_would_any.test.ts b/tests/features/deals_with_unknown_type_as_it_would_any.test.ts new file mode 100644 index 0000000..64ad11f --- /dev/null +++ b/tests/features/deals_with_unknown_type_as_it_would_any.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'Deals with unknown type as it would any', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + [index: string]: unknown + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([key, _value]) => (typeof key === "string")) + ) + } + `, + } +) diff --git a/tests/features/does_not_generate_empty_guard_files.test.ts b/tests/features/does_not_generate_empty_guard_files.test.ts new file mode 100644 index 0000000..03ac64d --- /dev/null +++ b/tests/features/does_not_generate_empty_guard_files.test.ts @@ -0,0 +1,9 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'Does not generate empty guard files', + { + 'test.ts': '', + }, + { 'test.ts': null } +) diff --git a/tests/features/does_not_touch_guardts_files_that_are_not_autogenerated.test.ts b/tests/features/does_not_touch_guardts_files_that_are_not_autogenerated.test.ts new file mode 100644 index 0000000..3d79cd1 --- /dev/null +++ b/tests/features/does_not_touch_guardts_files_that_are_not_autogenerated.test.ts @@ -0,0 +1,7 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'does not touch .guard.ts files that are not autogenerated', + { 'test.guard.ts': `alert("hello")` }, + { 'test.guard.ts': null } +) diff --git a/tests/features/generated_type_guards_for_arrays_of_any.test.ts b/tests/features/generated_type_guards_for_arrays_of_any.test.ts new file mode 100644 index 0000000..308eef1 --- /dev/null +++ b/tests/features/generated_type_guards_for_arrays_of_any.test.ts @@ -0,0 +1,28 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for arrays of any', + { + 'test.ts': ` + export interface Foo { + value: any[] + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Array.isArray(typedObj["value"]) + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_for_discriminated_unions.test.ts b/tests/features/generated_type_guards_for_discriminated_unions.test.ts new file mode 100644 index 0000000..80b71bc --- /dev/null +++ b/tests/features/generated_type_guards_for_discriminated_unions.test.ts @@ -0,0 +1,32 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for discriminated unions', + { + 'test.ts': ` + export type X = { type: 'a', value: number } | { type: 'b', value: string } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { X } from "./test"; + + export function isX(obj: unknown): obj is X { + const typedObj = obj as X + return ( + ((typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typedObj["type"] === "a" && + typeof typedObj["value"] === "number" || + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typedObj["type"] === "b" && + typeof typedObj["value"] === "string") + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_for_enums.test.ts b/tests/features/generated_type_guards_for_enums.test.ts new file mode 100644 index 0000000..d79267b --- /dev/null +++ b/tests/features/generated_type_guards_for_enums.test.ts @@ -0,0 +1,28 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for enums', + { + 'test.ts': ` + export enum Types{ + TheGood, + TheBad, + TheTypeSafe + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Types } from "./test"; + + export function isTypes(obj: unknown): obj is Types { + const typedObj = obj as Types + return ( + (typedObj === Types.TheGood || + typedObj === Types.TheBad || + typedObj === Types.TheTypeSafe) + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_for_intersection_type.test.ts b/tests/features/generated_type_guards_for_intersection_type.test.ts new file mode 100644 index 0000000..5a3be10 --- /dev/null +++ b/tests/features/generated_type_guards_for_intersection_type.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for intersection type', + { + 'test.ts': ` + export type X = { foo: number } & { bar: string } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { X } from "./test"; + + export function isX(obj: unknown): obj is X { + const typedObj = obj as X + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" && + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "string" + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_for_nested_arrays.test.ts b/tests/features/generated_type_guards_for_nested_arrays.test.ts new file mode 100644 index 0000000..73803cc --- /dev/null +++ b/tests/features/generated_type_guards_for_nested_arrays.test.ts @@ -0,0 +1,39 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for nested arrays', + { + 'test.ts': ` + export type Foo = { + value: Array<{ + value: Array + }> + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Array.isArray(typedObj["value"]) && + typedObj["value"].every((e: any) => + (e !== null && + typeof e === "object" || + typeof e === "function") && + Array.isArray(e["value"]) && + e["value"].every((e: any) => + typeof e === "number" + ) + ) + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_for_numeric_enums_in_optional_records.test.ts b/tests/features/generated_type_guards_for_numeric_enums_in_optional_records.test.ts new file mode 100644 index 0000000..61b6d9a --- /dev/null +++ b/tests/features/generated_type_guards_for_numeric_enums_in_optional_records.test.ts @@ -0,0 +1,49 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for numeric enums in optional records', + { + 'test.ts': ` + export enum Types{ + TheGood = 1, + TheBad, + TheTypeSafe + } + export interface TestItem { + room: Partial>>; + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Types, TestItem } from "./test"; + + export function isTypes(obj: unknown): obj is Types { + const typedObj = obj as Types + return ( + (typedObj === Types.TheGood || + typedObj === Types.TheBad || + typedObj === Types.TheTypeSafe) + ) + } + + export function isTestItem(obj: unknown): obj is TestItem { + const typedObj = obj as TestItem + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + (typedObj["room"] !== null && + typeof typedObj["room"] === "object" || + typeof typedObj["room"] === "function") && + (typeof typedObj["room"]["1"] === "undefined" || + typeof typedObj["room"]["1"] === "string") && + (typeof typedObj["room"]["2"] === "undefined" || + typeof typedObj["room"]["2"] === "string") && + (typeof typedObj["room"]["3"] === "undefined" || + typeof typedObj["room"]["3"] === "string") + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generated_type_guards_with_a_short_circuit_are_correctly_stripped_by_UglifyJS.test.ts b/tests/features/generated_type_guards_with_a_short_circuit_are_correctly_stripped_by_UglifyJS.test.ts new file mode 100644 index 0000000..a6ef546 --- /dev/null +++ b/tests/features/generated_type_guards_with_a_short_circuit_are_correctly_stripped_by_UglifyJS.test.ts @@ -0,0 +1,24 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards with a short circuit are correctly stripped by UglifyJS', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + foo: number, + bar: Foo | string | () => void, + baz: "foo" | "bar" + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': `"use strict";function isFoo(o){return!0}exports.__esModule=!0,exports.isFoo=void 0,exports.isFoo=isFoo;`, + }, + { + minifyOptions: { + compress: { global_defs: { DEBUG: true } }, + }, + options: { shortCircuitCondition: 'DEBUG', debug: false }, + } +) diff --git a/tests/features/generates_tuples.test.ts b/tests/features/generates_tuples.test.ts new file mode 100644 index 0000000..c76cc09 --- /dev/null +++ b/tests/features/generates_tuples.test.ts @@ -0,0 +1,28 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates tuples', + { + 'test.ts': ` + export interface A { + b: [number] + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { A } from "./test"; + + export function isA(obj: unknown): obj is A { + const typedObj = obj as A + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Array.isArray(typedObj["b"]) && + typeof typedObj["b"][0] === "number" + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generates_type_guards_for_JSDoc_see_with_link_tag.test.ts b/tests/features/generates_type_guards_for_JSDoc_see_with_link_tag.test.ts new file mode 100644 index 0000000..5c4c54c --- /dev/null +++ b/tests/features/generates_type_guards_for_JSDoc_see_with_link_tag.test.ts @@ -0,0 +1,22 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for JSDoc @see with @link tag', + { + 'test.ts': ` + /** @see {@link isBool} ts-auto-guard:type-guard */ + export type Bool = boolean`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Bool } from "./test"; + + export function isBool(obj: unknown): obj is Bool { + const typedObj = obj as Bool + return ( + typeof typedObj === "boolean" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_a_Pick_type.test.ts b/tests/features/generates_type_guards_for_a_Pick_type.test.ts new file mode 100644 index 0000000..e5b2873 --- /dev/null +++ b/tests/features/generates_type_guards_for_a_Pick_type.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for a Pick<> type', + { + 'test.ts': ` + interface Bar { + foo: number, + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = Pick`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_an_object_literal_type.test.ts b/tests/features/generates_type_guards_for_an_object_literal_type.test.ts new file mode 100644 index 0000000..76044cd --- /dev/null +++ b/tests/features/generates_type_guards_for_an_object_literal_type.test.ts @@ -0,0 +1,27 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for an object literal type', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + foo: number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_boolean.test.ts b/tests/features/generates_type_guards_for_boolean.test.ts new file mode 100644 index 0000000..50e26a8 --- /dev/null +++ b/tests/features/generates_type_guards_for_boolean.test.ts @@ -0,0 +1,22 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for boolean', + { + 'test.ts': ` + /** @see {isBool} ts-auto-guard:type-guard */ + export type Bool = boolean`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Bool } from "./test"; + + export function isBool(obj: unknown): obj is Bool { + const typedObj = obj as Bool + return ( + typeof typedObj === "boolean" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_dynamic_object_keys,_including_when_mixed_with_static_keys.test.ts b/tests/features/generates_type_guards_for_dynamic_object_keys,_including_when_mixed_with_static_keys.test.ts new file mode 100644 index 0000000..be63363 --- /dev/null +++ b/tests/features/generates_type_guards_for_dynamic_object_keys,_including_when_mixed_with_static_keys.test.ts @@ -0,0 +1,40 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for dynamic object keys, including when mixed with static keys', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + someKey: "some" | "key" + [index: string]: "dynamic" | "string" + [index: number]: "also-dynamic" | "number" + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + (typedObj["someKey"] === "some" || + typedObj["someKey"] === "key") && + Object.entries(typedObj) + .filter(([key]) => !["someKey"].includes(key)) + .every(([key, value]) => ((value === "string" || + value === "dynamic") && + typeof key === "string" || + (value === "number" || + value === "also-dynamic") && + typeof key === "number")) + ) + } + `, + } +) diff --git a/tests/features/generates_type_guards_for_empty_object_if_exportAll_is_true.test.ts b/tests/features/generates_type_guards_for_empty_object_if_exportAll_is_true.test.ts new file mode 100644 index 0000000..08ab659 --- /dev/null +++ b/tests/features/generates_type_guards_for_empty_object_if_exportAll_is_true.test.ts @@ -0,0 +1,24 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for empty object if exportAll is true', + { + 'test.ts': ` + export interface Empty {}`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Empty } from "./test"; + + export function isEmpty(obj: unknown): obj is Empty { + const typedObj = obj as Empty + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") + ) + }`, + }, + { options: { exportAll: true, debug: false } } +) diff --git a/tests/features/generates_type_guards_for_interface_extending_object_type.test.ts b/tests/features/generates_type_guards_for_interface_extending_object_type.test.ts new file mode 100644 index 0000000..d72a58f --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_extending_object_type.test.ts @@ -0,0 +1,32 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface extending object type', + { + 'test.ts': ` + export type Bar = { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo extends Bar { + foo: number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "number" && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_extending_object_type_with_type_guard.test.ts b/tests/features/generates_type_guards_for_interface_extending_object_type_with_type_guard.test.ts new file mode 100644 index 0000000..efd5f0e --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_extending_object_type_with_type_guard.test.ts @@ -0,0 +1,40 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface extending object type with type guard', + { + 'test.ts': ` + /** @see {isBar} ts-auto-guard:type-guard */ + export type Bar = { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo extends Bar { + foo: number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Bar, Foo } from "./test"; + + export function isBar(obj: unknown): obj is Bar { + const typedObj = obj as Bar + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "number" + ) + } + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + isBar(typedObj) as boolean && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_extending_other_interface.test.ts b/tests/features/generates_type_guards_for_interface_extending_other_interface.test.ts new file mode 100644 index 0000000..a61dfc9 --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_extending_other_interface.test.ts @@ -0,0 +1,32 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface extending other interface', + { + 'test.ts': ` + interface Bar { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo extends Bar { + foo: number, + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "number" && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_extending_other_interface_with_type_guard.test.ts b/tests/features/generates_type_guards_for_interface_extending_other_interface_with_type_guard.test.ts new file mode 100644 index 0000000..5c4d421 --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_extending_other_interface_with_type_guard.test.ts @@ -0,0 +1,40 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface extending other interface with type guard', + { + 'test.ts': ` + /** @see {isBar} ts-auto-guard:type-guard */ + export interface Bar { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo extends Bar { + foo: number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Bar, Foo } from "./test"; + + export function isBar(obj: unknown): obj is Bar { + const typedObj = obj as Bar + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "number" + ) + } + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + isBar(typedObj) as boolean && + typeof typedObj["foo"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_properties_with_numerical_names.test.ts b/tests/features/generates_type_guards_for_interface_properties_with_numerical_names.test.ts new file mode 100644 index 0000000..bdbaa99 --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_properties_with_numerical_names.test.ts @@ -0,0 +1,29 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface properties with numerical names', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + "1": number, + "2": string + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["1"] === "number" && + typeof typedObj["2"] === "string" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_property_with_empty_string_as_name.test.ts b/tests/features/generates_type_guards_for_interface_property_with_empty_string_as_name.test.ts new file mode 100644 index 0000000..2998221 --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_property_with_empty_string_as_name.test.ts @@ -0,0 +1,27 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface property with empty string as name', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + "": number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj[""] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts b/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts new file mode 100644 index 0000000..c1e4833 --- /dev/null +++ b/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts @@ -0,0 +1,34 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface with optional field', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo?: number, + bar: number | undefined, + baz?: number | undefined + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + ( typeof typedObj["foo"] === "undefined" || + typeof typedObj["foo"] === "number" ) && + ( typeof typedObj["bar"] === "undefined" || + typeof typedObj["bar"] === "number" ) && + ( typeof typedObj["baz"] === "undefined" || + typeof typedObj["baz"] === "number" ) + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_mapped_types.test.ts b/tests/features/generates_type_guards_for_mapped_types.test.ts new file mode 100644 index 0000000..dbfed5d --- /dev/null +++ b/tests/features/generates_type_guards_for_mapped_types.test.ts @@ -0,0 +1,53 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for mapped types', + { + 'test.ts': ` + /** @see {isPropertyValueType} ts-auto-guard:type-guard */ + export type PropertyValueType = {value: string}; + + /** @see {isPropertyName} ts-auto-guard:type-guard */ + export type PropertyName = 'name' | 'value'; + + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + [key in PropertyName]: PropertyValueType + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { PropertyValueType, PropertyName, Foo } from "./test"; + + export function isPropertyValueType(obj: unknown): obj is PropertyValueType { + const typedObj = obj as PropertyValueType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["value"] === "string" + ) + } + + export function isPropertyName(obj: unknown): obj is PropertyName { + const typedObj = obj as PropertyName + return ( + (typedObj === "name" || + typedObj === "value") + ) + } + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + isPropertyValueType(typedObj["name"]) as boolean && + isPropertyValueType(typedObj["value"]) as boolean + ) + } + `, + } +) diff --git a/tests/features/generates_type_guards_for_nested_interface.test.ts b/tests/features/generates_type_guards_for_nested_interface.test.ts new file mode 100644 index 0000000..62f281e --- /dev/null +++ b/tests/features/generates_type_guards_for_nested_interface.test.ts @@ -0,0 +1,34 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for nested interface', + { + 'test.ts': ` + interface Bar { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: Bar, + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + (typedObj["foo"] !== null && + typeof typedObj["foo"] === "object" || + typeof typedObj["foo"] === "function") && + typeof typedObj["foo"]["bar"] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_nested_interface_with_type_guard.test.ts b/tests/features/generates_type_guards_for_nested_interface_with_type_guard.test.ts new file mode 100644 index 0000000..895d4a0 --- /dev/null +++ b/tests/features/generates_type_guards_for_nested_interface_with_type_guard.test.ts @@ -0,0 +1,42 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for nested interface with type guard', + { + 'test.ts': ` + /** @see {isBar} ts-auto-guard:type-guard */ + export interface Bar { + bar: number + } + + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: Bar, + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Bar, Foo } from "./test"; + + export function isBar(obj: unknown): obj is Bar { + const typedObj = obj as Bar + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["bar"] === "number" + ) + } + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + isBar(typedObj["foo"]) as boolean + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_property_with_non_alphanumeric_name_.test.ts b/tests/features/generates_type_guards_for_property_with_non_alphanumeric_name_.test.ts new file mode 100644 index 0000000..f26094c --- /dev/null +++ b/tests/features/generates_type_guards_for_property_with_non_alphanumeric_name_.test.ts @@ -0,0 +1,75 @@ +// characters that are currently not supported include double quotes, backslashes and newlines +import { testProcessProject } from '../generate' + +const nonAlphanumericCharacterPropertyNames = [ + '\0', + ' ', + '-', + '+', + '*', + '/', + '.', + 'foo bar', + 'foo-bar', + 'foo+bar', + 'foo*bar', + 'foo/bar', + 'foo.bar', + "'foobar'", + '#hashtag', + '1337_leadingNumbers', +] + +for (const propertyName of nonAlphanumericCharacterPropertyNames) { + testProcessProject( + `generates type guards for interface property with non-alphanumeric name '${propertyName}'`, + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + "${propertyName}": number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["${propertyName}"] === "number" + ) + }`, + } + ) + + testProcessProject( + `generates type guards for type property with non-alphanumeric name '${propertyName}'`, + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + "${propertyName}": number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["${propertyName}"] === "number" + ) + }`, + } + ) +} diff --git a/tests/features/generates_type_guards_for_record_types.test.ts b/tests/features/generates_type_guards_for_record_types.test.ts new file mode 100644 index 0000000..4614b8f --- /dev/null +++ b/tests/features/generates_type_guards_for_record_types.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for Record types', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export type TestType = Record + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([key, value]) => ((value === "string" || + value === "dynamic") && + typeof key === "string")) + ) + } + `, + } +) diff --git a/tests/features/generates_type_guards_for_recursive_types.test.ts b/tests/features/generates_type_guards_for_recursive_types.test.ts new file mode 100644 index 0000000..81eb24c --- /dev/null +++ b/tests/features/generates_type_guards_for_recursive_types.test.ts @@ -0,0 +1,68 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for recursive types', + { + 'test.ts': ` + /** @see {isBranch1} ts-auto-guard:type-guard */ + export type Branch1 = Branch1[] | string; + + /** @see {isBranch2} ts-auto-guard:type-guard */ + export type Branch2 = { branches: Branch2[] } | string; + + /** @see {isBranch3} ts-auto-guard:type-guard */ + export type Branch3 = { branches: Branch3[] } | {branches: Branch3 }[] | string; + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Branch1, Branch2, Branch3 } from "./test"; + + export function isBranch1(obj: unknown): obj is Branch1 { + const typedObj = obj as Branch1 + return ( + (typeof typedObj === "string" || + Array.isArray(typedObj) && + typedObj.every((e: any) => + isBranch1(e) as boolean + )) + ) + } + + export function isBranch2(obj: unknown): obj is Branch2 { + const typedObj = obj as Branch2 + return ( + (typeof typedObj === "string" || + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Array.isArray(typedObj["branches"]) && + typedObj["branches"].every((e: any) => + isBranch2(e) as boolean + )) + ) + } + + export function isBranch3(obj: unknown): obj is Branch3 { + const typedObj = obj as Branch3 + return ( + (typeof typedObj === "string" || + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Array.isArray(typedObj["branches"]) && + typedObj["branches"].every((e: any) => + isBranch3(e) as boolean + ) || + Array.isArray(typedObj) && + typedObj.every((e: any) => + (e !== null && + typeof e === "object" || + typeof e === "function") && + isBranch3(e["branches"]) as boolean + )) + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_simple_interface.test.ts b/tests/features/generates_type_guards_for_simple_interface.test.ts new file mode 100644 index 0000000..a6560df --- /dev/null +++ b/tests/features/generates_type_guards_for_simple_interface.test.ts @@ -0,0 +1,29 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for simple interface', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: number, + bar: string + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" && + typeof typedObj["bar"] === "string" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_type_properties_with_numerical_names.test.ts b/tests/features/generates_type_guards_for_type_properties_with_numerical_names.test.ts new file mode 100644 index 0000000..cd28344 --- /dev/null +++ b/tests/features/generates_type_guards_for_type_properties_with_numerical_names.test.ts @@ -0,0 +1,29 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for type properties with numerical names', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + "1": number, + "2": string + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["1"] === "number" && + typeof typedObj["2"] === "string" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_for_type_property_with_empty_string_as_name.test.ts b/tests/features/generates_type_guards_for_type_property_with_empty_string_as_name.test.ts new file mode 100644 index 0000000..394ff12 --- /dev/null +++ b/tests/features/generates_type_guards_for_type_property_with_empty_string_as_name.test.ts @@ -0,0 +1,27 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for type property with empty string as name', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + "": number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj[""] === "number" + ) + }`, + } +) diff --git a/tests/features/generates_type_guards_with_a_short_circuit.test.ts b/tests/features/generates_type_guards_with_a_short_circuit.test.ts new file mode 100644 index 0000000..dc781b6 --- /dev/null +++ b/tests/features/generates_type_guards_with_a_short_circuit.test.ts @@ -0,0 +1,31 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards with a short circuit', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo = { + foo: number + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + if (DEBUG) return true + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" + ) + }`, + }, + { + options: { shortCircuitCondition: 'DEBUG', debug: false }, + } +) diff --git a/tests/features/imports_and_uses_generated_type_guard_if_the_type_is_used_in_another_file.test.ts b/tests/features/imports_and_uses_generated_type_guard_if_the_type_is_used_in_another_file.test.ts new file mode 100644 index 0000000..dae1fd4 --- /dev/null +++ b/tests/features/imports_and_uses_generated_type_guard_if_the_type_is_used_in_another_file.test.ts @@ -0,0 +1,51 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'imports and uses generated type guard if the type is used in another file', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + someKey: string | number + } + `, + 'test-list.ts': ` + import { TestType } from './test' + + /** @see {isTestTypeList} ts-auto-guard:type-guard */ + export type TestTypeList = Array + `, + }, + { + 'test.ts': null, + 'test-list.ts': null, + 'test-list.guard.ts': ` + import { isTestType } from "./test.guard"; + import { TestTypeList } from "./test-list"; + + export function isTestTypeList(obj: unknown): obj is TestTypeList { + const typedObj = obj as TestTypeList + return ( + Array.isArray(typedObj) && + typedObj.every((e: any) => + isTestType(e) as boolean + ) + ) + } + `, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + (typeof typedObj["someKey"] === "string" || + typeof typedObj["someKey"] === "number") + ) + } + `, + } +) diff --git a/tests/features/prefixes_key_with_underscore_if_it_goes_unused.test.ts b/tests/features/prefixes_key_with_underscore_if_it_goes_unused.test.ts new file mode 100644 index 0000000..3bf9242 --- /dev/null +++ b/tests/features/prefixes_key_with_underscore_if_it_goes_unused.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'prefixes key with underscore if it goes unused', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + [index: any]: string + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([_key, value]) => (typeof value === "string")) + ) + } + `, + } +) diff --git a/tests/features/prefixes_value_with_underscore_if_it_goes_unused.test.ts b/tests/features/prefixes_value_with_underscore_if_it_goes_unused.test.ts new file mode 100644 index 0000000..bee40f7 --- /dev/null +++ b/tests/features/prefixes_value_with_underscore_if_it_goes_unused.test.ts @@ -0,0 +1,30 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'prefixes value with underscore if it goes unused', + { + 'test.ts': ` + /** @see {isTestType} ts-auto-guard:type-guard */ + export interface TestType { + [index: string]: any + } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([key, _value]) => (typeof key === "string")) + ) + } + `, + } +) diff --git a/tests/features/rejects_invalid_guardFileNames.test.ts b/tests/features/rejects_invalid_guardFileNames.test.ts new file mode 100644 index 0000000..e02b6af --- /dev/null +++ b/tests/features/rejects_invalid_guardFileNames.test.ts @@ -0,0 +1,14 @@ +import { testProcessProject } from '../generate' + +const invalidGuardFileNameCharacters = ['*', '/'] +for (const invalidCharacter of invalidGuardFileNameCharacters) { + testProcessProject( + `rejects invalid guardFileNames: ${invalidCharacter}`, + {}, + {}, + { + options: { guardFileName: `f${invalidCharacter}o` }, + throws: /guardFileName/, + } + ) +} diff --git a/tests/features/removes_correct_guardts_files_when_guardFileName_is_set.test.ts b/tests/features/removes_correct_guardts_files_when_guardFileName_is_set.test.ts new file mode 100644 index 0000000..f1e2796 --- /dev/null +++ b/tests/features/removes_correct_guardts_files_when_guardFileName_is_set.test.ts @@ -0,0 +1,11 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'removes correct .guard.ts files when guardFileName is set', + { + 'test.foo.ts': `/* WARNING: Do not manually change this file. */alert("hello")`, + 'test.guard.ts': `/* WARNING: Do not manually change this file. */alert("hello")`, + }, + { 'test.guard.ts': null }, + { options: { guardFileName: 'foo' } } +) diff --git a/tests/features/removes_existing_guardts_files.test.ts b/tests/features/removes_existing_guardts_files.test.ts new file mode 100644 index 0000000..23c35c9 --- /dev/null +++ b/tests/features/removes_existing_guardts_files.test.ts @@ -0,0 +1,9 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'removes existing .guard.ts files', + { + 'test.guard.ts': `/* WARNING: Do not manually change this file. */ alert("hello")`, + }, + {} +) diff --git a/tests/features/show_debug_info.test.ts b/tests/features/show_debug_info.test.ts new file mode 100644 index 0000000..8a8ade4 --- /dev/null +++ b/tests/features/show_debug_info.test.ts @@ -0,0 +1,72 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'show debug info', + { + [`foo/bar/test.ts`]: ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: number, + bar: Bar, + bars: Array + } + + /** @see {isBar} ts-auto-guard:type-guard */ + export interface Bar { + bar: number, + } + + `, + }, + { + [`foo/bar/test.ts`]: null, + [`foo/bar/test.guard.ts`]: ` + import { Foo, Bar } from "./test"; + + function evaluate( + isCorrect: boolean, + varName: string, + expected: string, + actual: any + ): boolean { + if (!isCorrect) { + console.error( + \`\${varName} type mismatch, expected: \${expected}, found:\`, + actual + ) + } + return isCorrect + } + + export function isFoo(obj: unknown, argumentName: string = "foo"): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + evaluate(typeof typedObj["foo"] === "number", \`\${argumentName}["foo"]\`, "number", typedObj["foo"]) && + evaluate(isBar(typedObj["bar"]) as boolean, \`\${argumentName}["bar"]\`, "import(\\"/foo/bar/test\\").Bar", typedObj["bar"]) && + evaluate(Array.isArray(typedObj["bars"]) && + typedObj["bars"].every((e: any) => + isBar(e) as boolean + ), \`\${argumentName}["bars"]\`, "import(\\"/foo/bar/test\\").Bar[]", typedObj["bars"]) + ) + } + + export function isBar(obj: unknown, argumentName: string = "bar"): obj is Bar { + const typedObj = obj as Bar + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + evaluate(typeof typedObj["bar"] === "number", \`\${argumentName}["bar"]\`, "number", typedObj["bar"]) + ) + } + `, + }, + { + options: { + debug: true, + }, + } +) diff --git a/tests/features/skips_checking_any_type_in_array.test.ts b/tests/features/skips_checking_any_type_in_array.test.ts new file mode 100644 index 0000000..a2dbc07 --- /dev/null +++ b/tests/features/skips_checking_any_type_in_array.test.ts @@ -0,0 +1,21 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'skips checking any type in array', + { + 'test.ts': `export type A = any[]`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { A } from "./test"; + + export function isA(obj: unknown): obj is A { + const typedObj = obj as A + return ( + Array.isArray(typedObj) + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/type_that_is_an_alias_to_an_interface_has_a_different_typeguard_name.test.ts b/tests/features/type_that_is_an_alias_to_an_interface_has_a_different_typeguard_name.test.ts new file mode 100644 index 0000000..bd595cb --- /dev/null +++ b/tests/features/type_that_is_an_alias_to_an_interface_has_a_different_typeguard_name.test.ts @@ -0,0 +1,42 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'type that is an alias to an interface has a different typeguard name', + { + 'test.ts': ` + export interface TestType { + [index: any]: string + } + export type SecondaryTestType = TestType + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { TestType, SecondaryTestType } from "./test"; + + export function isTestType(obj: unknown): obj is TestType { + const typedObj = obj as TestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([_key, value]) => (typeof value === "string")) + ) + } + + export function isSecondaryTestType(obj: unknown): obj is SecondaryTestType { + const typedObj = obj as SecondaryTestType + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + Object.entries(typedObj) + .every(([_key, value]) => (typeof value === "string")) + ) + } + `, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/uses_correct_import_file_name_if_guard_file_is_renamed.test.ts b/tests/features/uses_correct_import_file_name_if_guard_file_is_renamed.test.ts new file mode 100644 index 0000000..7aaff29 --- /dev/null +++ b/tests/features/uses_correct_import_file_name_if_guard_file_is_renamed.test.ts @@ -0,0 +1,36 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'uses correct import file name if guard file is renamed', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export interface Foo { + foo: number, + bar: string + }`, + }, + { + 'test.ts': null, + 'test.debug.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["foo"] === "number" && + typeof typedObj["bar"] === "string" + ) + }`, + }, + { + options: { + guardFileName: 'debug', + importGuards: 'CustomGuardAlias', + }, + skip: true, + } +) diff --git a/tests/features/works_for_any_type.test.ts b/tests/features/works_for_any_type.test.ts new file mode 100644 index 0000000..a835c1a --- /dev/null +++ b/tests/features/works_for_any_type.test.ts @@ -0,0 +1,21 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'works for any type', + { + 'test.ts': `export type A = any`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { A } from "./test"; + + export function isA(obj: unknown): obj is A { + const typedObj = obj as A + return ( + true + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/works_for_unknown_type.test.ts b/tests/features/works_for_unknown_type.test.ts new file mode 100644 index 0000000..7616501 --- /dev/null +++ b/tests/features/works_for_unknown_type.test.ts @@ -0,0 +1,21 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'works for unknown type', + { + 'test.ts': `export type A = unknown`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { A } from "./test"; + + export function isA(obj: unknown): obj is A { + const typedObj = obj as A + return ( + true + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/generate.ts b/tests/generate.ts index f033c64..fe1150d 100644 --- a/tests/generate.ts +++ b/tests/generate.ts @@ -19,12 +19,12 @@ interface ITestOptions { throws?: RegExp | typeof Error } -function testProcessProject( +export function testProcessProject( typeDescription: string, input: { readonly [filename: string]: string }, output: { readonly [filename: string]: string | null }, { skip, only, options, minifyOptions, throws }: ITestOptions = {} -) { +): void { const fn = skip ? test.skip : only ? test.only : test fn(typeDescription, t => { const project = createProject() @@ -88,1705 +88,3 @@ function testProcessProject( t.end() }) } - -testProcessProject( - 'removes existing .guard.ts files', - { - 'test.guard.ts': `/* WARNING: Do not manually change this file. */ alert("hello")`, - }, - {} -) - -testProcessProject( - 'does not touch .guard.ts files that are not autogenerated', - { 'test.guard.ts': `alert("hello")` }, - { 'test.guard.ts': null } -) - -testProcessProject( - 'removes correct .guard.ts files when guardFileName is set', - { - 'test.foo.ts': `/* WARNING: Do not manually change this file. */alert("hello")`, - 'test.guard.ts': `/* WARNING: Do not manually change this file. */alert("hello")`, - }, - { 'test.guard.ts': null }, - { options: { guardFileName: 'foo' } } -) - -const invalidGuardFileNameCharacters = ['*', '/'] -for (const invalidCharacter of invalidGuardFileNameCharacters) { - testProcessProject( - `rejects invalid guardFileNames: ${invalidCharacter}`, - {}, - {}, - { - options: { guardFileName: `f${invalidCharacter}o` }, - throws: /guardFileName/, - } - ) -} - -testProcessProject( - 'generates type guards for empty object if exportAll is true', - { - 'test.ts': ` - export interface Empty {}`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Empty } from "./test"; - - export function isEmpty(obj: unknown): obj is Empty { - const typedObj = obj as Empty - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") - ) - }`, - }, - { options: { exportAll: true, debug: false } } -) - -testProcessProject( - 'generates type guards for JSDoc @see with @link tag', - { - 'test.ts': ` - /** @see {@link isBool} ts-auto-guard:type-guard */ - export type Bool = boolean`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Bool } from "./test"; - - export function isBool(obj: unknown): obj is Bool { - const typedObj = obj as Bool - return ( - typeof typedObj === "boolean" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for boolean', - { - 'test.ts': ` - /** @see {isBool} ts-auto-guard:type-guard */ - export type Bool = boolean`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Bool } from "./test"; - - export function isBool(obj: unknown): obj is Bool { - const typedObj = obj as Bool - return ( - typeof typedObj === "boolean" - ) - }`, - } -) - -testProcessProject( - 'allows the name of the guard file file to be specified', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: number, - bar: string - }`, - }, - { - 'test.ts': null, - 'test.debug.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" && - typeof typedObj["bar"] === "string" - ) - }`, - }, - { - options: { - guardFileName: 'debug', - }, - } -) - -testProcessProject( - 'show debug info', - { - [`foo/bar/test.ts`]: ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: number, - bar: Bar, - bars: Array - } - - /** @see {isBar} ts-auto-guard:type-guard */ - export interface Bar { - bar: number, - } - - `, - }, - { - [`foo/bar/test.ts`]: null, - [`foo/bar/test.guard.ts`]: ` - import { Foo, Bar } from "./test"; - - function evaluate( - isCorrect: boolean, - varName: string, - expected: string, - actual: any - ): boolean { - if (!isCorrect) { - console.error( - \`\${varName} type mismatch, expected: \${expected}, found:\`, - actual - ) - } - return isCorrect - } - - export function isFoo(obj: unknown, argumentName: string = "foo"): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - evaluate(typeof typedObj["foo"] === "number", \`\${argumentName}["foo"]\`, "number", typedObj["foo"]) && - evaluate(isBar(typedObj["bar"]) as boolean, \`\${argumentName}["bar"]\`, "import(\\"/foo/bar/test\\").Bar", typedObj["bar"]) && - evaluate(Array.isArray(typedObj["bars"]) && - typedObj["bars"].every((e: any) => - isBar(e) as boolean - ), \`\${argumentName}["bars"]\`, "import(\\"/foo/bar/test\\").Bar[]", typedObj["bars"]) - ) - } - - export function isBar(obj: unknown, argumentName: string = "bar"): obj is Bar { - const typedObj = obj as Bar - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - evaluate(typeof typedObj["bar"] === "number", \`\${argumentName}["bar"]\`, "number", typedObj["bar"]) - ) - } - `, - }, - { - options: { - debug: true, - }, - } -) - -testProcessProject( - 'uses correct import file name if guard file is renamed', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: number, - bar: string - }`, - }, - { - 'test.ts': null, - 'test.debug.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" && - typeof typedObj["bar"] === "string" - ) - }`, - }, - { - options: { - guardFileName: 'debug', - importGuards: 'CustomGuardAlias', - }, - skip: true, - } -) - -testProcessProject( - 'generates type guards for simple interface', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: number, - bar: string - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" && - typeof typedObj["bar"] === "string" - ) - }`, - } -) - -// characters that are currently not supported include double quotes, backslashes and newlines -const nonAlphanumericCharacterPropertyNames = [ - '\0', - ' ', - '-', - '+', - '*', - '/', - '.', - 'foo bar', - 'foo-bar', - 'foo+bar', - 'foo*bar', - 'foo/bar', - 'foo.bar', - "'foobar'", - '#hashtag', - '1337_leadingNumbers', -] - -for (const propertyName of nonAlphanumericCharacterPropertyNames) { - testProcessProject( - `generates type guards for interface property with non-alphanumeric name '${propertyName}'`, - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - "${propertyName}": number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["${propertyName}"] === "number" - ) - }`, - } - ) - - testProcessProject( - `generates type guards for type property with non-alphanumeric name '${propertyName}'`, - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - "${propertyName}": number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["${propertyName}"] === "number" - ) - }`, - } - ) -} - -testProcessProject( - 'generates type guards for interface properties with numerical names', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - "1": number, - "2": string - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["1"] === "number" && - typeof typedObj["2"] === "string" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for type properties with numerical names', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - "1": number, - "2": string - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["1"] === "number" && - typeof typedObj["2"] === "string" - ) - }`, - } -) -testProcessProject( - 'generates type guards for interface property with empty string as name', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - "": number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj[""] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for type property with empty string as name', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - "": number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj[""] === "number" - ) - }`, - } -) - -testProcessProject( - 'correctly handles default export', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - interface Foo { - foo: number, - bar: string - } - - export default Foo`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import Foo from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" && - typeof typedObj["bar"] === "string" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for interface with optional field', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo?: number, - bar: number | undefined, - baz?: number | undefined - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - ( typeof typedObj["foo"] === "undefined" || - typeof typedObj["foo"] === "number" ) && - ( typeof typedObj["bar"] === "undefined" || - typeof typedObj["bar"] === "number" ) && - ( typeof typedObj["baz"] === "undefined" || - typeof typedObj["baz"] === "number" ) - ) - }`, - } -) - -testProcessProject( - 'generates type guards for nested interface', - { - 'test.ts': ` - interface Bar { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: Bar, - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - (typedObj["foo"] !== null && - typeof typedObj["foo"] === "object" || - typeof typedObj["foo"] === "function") && - typeof typedObj["foo"]["bar"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for nested interface with type guard', - { - 'test.ts': ` - /** @see {isBar} ts-auto-guard:type-guard */ - export interface Bar { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo { - foo: Bar, - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Bar, Foo } from "./test"; - - export function isBar(obj: unknown): obj is Bar { - const typedObj = obj as Bar - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "number" - ) - } - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - isBar(typedObj["foo"]) as boolean - ) - }`, - } -) - -testProcessProject( - 'generates type guards for interface extending other interface', - { - 'test.ts': ` - interface Bar { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo extends Bar { - foo: number, - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "number" && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for interface extending other interface with type guard', - { - 'test.ts': ` - /** @see {isBar} ts-auto-guard:type-guard */ - export interface Bar { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo extends Bar { - foo: number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Bar, Foo } from "./test"; - - export function isBar(obj: unknown): obj is Bar { - const typedObj = obj as Bar - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "number" - ) - } - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - isBar(typedObj) as boolean && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for interface extending object type', - { - 'test.ts': ` - export type Bar = { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo extends Bar { - foo: number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "number" && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for interface extending object type with type guard', - { - 'test.ts': ` - /** @see {isBar} ts-auto-guard:type-guard */ - export type Bar = { - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export interface Foo extends Bar { - foo: number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Bar, Foo } from "./test"; - - export function isBar(obj: unknown): obj is Bar { - const typedObj = obj as Bar - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "number" - ) - } - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - isBar(typedObj) as boolean && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for an object literal type', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - foo: number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards for a Pick<> type', - { - 'test.ts': ` - interface Bar { - foo: number, - bar: number - } - - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = Pick`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" - ) - }`, - } -) - -testProcessProject( - 'generates type guards with a short circuit', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - foo: number - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - if (DEBUG) return true - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" - ) - }`, - }, - { - options: { shortCircuitCondition: 'DEBUG', debug: false }, - } -) - -testProcessProject( - 'generated type guards with a short circuit are correctly stripped by UglifyJS', - { - 'test.ts': ` - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - foo: number, - bar: Foo | string | () => void, - baz: "foo" | "bar" - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': `"use strict";function isFoo(o){return!0}exports.__esModule=!0,exports.isFoo=void 0,exports.isFoo=isFoo;`, - }, - { - minifyOptions: { - compress: { global_defs: { DEBUG: true } }, - }, - options: { shortCircuitCondition: 'DEBUG', debug: false }, - } -) - -testProcessProject( - 'generates type guards for mapped types', - { - 'test.ts': ` - /** @see {isPropertyValueType} ts-auto-guard:type-guard */ - export type PropertyValueType = {value: string}; - - /** @see {isPropertyName} ts-auto-guard:type-guard */ - export type PropertyName = 'name' | 'value'; - - /** @see {isFoo} ts-auto-guard:type-guard */ - export type Foo = { - [key in PropertyName]: PropertyValueType - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { PropertyValueType, PropertyName, Foo } from "./test"; - - export function isPropertyValueType(obj: unknown): obj is PropertyValueType { - const typedObj = obj as PropertyValueType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["value"] === "string" - ) - } - - export function isPropertyName(obj: unknown): obj is PropertyName { - const typedObj = obj as PropertyName - return ( - (typedObj === "name" || - typedObj === "value") - ) - } - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - isPropertyValueType(typedObj["name"]) as boolean && - isPropertyValueType(typedObj["value"]) as boolean - ) - } - `, - } -) - -testProcessProject( - 'generates type guards for recursive types', - { - 'test.ts': ` - /** @see {isBranch1} ts-auto-guard:type-guard */ - export type Branch1 = Branch1[] | string; - - /** @see {isBranch2} ts-auto-guard:type-guard */ - export type Branch2 = { branches: Branch2[] } | string; - - /** @see {isBranch3} ts-auto-guard:type-guard */ - export type Branch3 = { branches: Branch3[] } | {branches: Branch3 }[] | string; - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Branch1, Branch2, Branch3 } from "./test"; - - export function isBranch1(obj: unknown): obj is Branch1 { - const typedObj = obj as Branch1 - return ( - (typeof typedObj === "string" || - Array.isArray(typedObj) && - typedObj.every((e: any) => - isBranch1(e) as boolean - )) - ) - } - - export function isBranch2(obj: unknown): obj is Branch2 { - const typedObj = obj as Branch2 - return ( - (typeof typedObj === "string" || - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Array.isArray(typedObj["branches"]) && - typedObj["branches"].every((e: any) => - isBranch2(e) as boolean - )) - ) - } - - export function isBranch3(obj: unknown): obj is Branch3 { - const typedObj = obj as Branch3 - return ( - (typeof typedObj === "string" || - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Array.isArray(typedObj["branches"]) && - typedObj["branches"].every((e: any) => - isBranch3(e) as boolean - ) || - Array.isArray(typedObj) && - typedObj.every((e: any) => - (e !== null && - typeof e === "object" || - typeof e === "function") && - isBranch3(e["branches"]) as boolean - )) - ) - }`, - } -) - -testProcessProject( - 'generated type guards for discriminated unions', - { - 'test.ts': ` - export type X = { type: 'a', value: number } | { type: 'b', value: string } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { X } from "./test"; - - export function isX(obj: unknown): obj is X { - const typedObj = obj as X - return ( - ((typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typedObj["type"] === "a" && - typeof typedObj["value"] === "number" || - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typedObj["type"] === "b" && - typeof typedObj["value"] === "string") - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'generated type guards for enums', - { - 'test.ts': ` - export enum Types{ - TheGood, - TheBad, - TheTypeSafe - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Types } from "./test"; - - export function isTypes(obj: unknown): obj is Types { - const typedObj = obj as Types - return ( - (typedObj === Types.TheGood || - typedObj === Types.TheBad || - typedObj === Types.TheTypeSafe) - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'generated type guards for numeric enums in optional records', - { - 'test.ts': ` - export enum Types{ - TheGood = 1, - TheBad, - TheTypeSafe - } - export interface TestItem { - room: Partial>>; - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Types, TestItem } from "./test"; - - export function isTypes(obj: unknown): obj is Types { - const typedObj = obj as Types - return ( - (typedObj === Types.TheGood || - typedObj === Types.TheBad || - typedObj === Types.TheTypeSafe) - ) - } - - export function isTestItem(obj: unknown): obj is TestItem { - const typedObj = obj as TestItem - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - (typedObj["room"] !== null && - typeof typedObj["room"] === "object" || - typeof typedObj["room"] === "function") && - (typeof typedObj["room"]["1"] === "undefined" || - typeof typedObj["room"]["1"] === "string") && - (typeof typedObj["room"]["2"] === "undefined" || - typeof typedObj["room"]["2"] === "string") && - (typeof typedObj["room"]["3"] === "undefined" || - typeof typedObj["room"]["3"] === "string") - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'generated type guards for arrays of any', - { - 'test.ts': ` - export interface Foo { - value: any[] - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Array.isArray(typedObj["value"]) - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'generated type guards for nested arrays', - { - 'test.ts': ` - export type Foo = { - value: Array<{ - value: Array - }> - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { Foo } from "./test"; - - export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Array.isArray(typedObj["value"]) && - typedObj["value"].every((e: any) => - (e !== null && - typeof e === "object" || - typeof e === "function") && - Array.isArray(e["value"]) && - e["value"].every((e: any) => - typeof e === "number" - ) - ) - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'type that is an alias to an interface has a different typeguard name', - { - 'test.ts': ` - export interface TestType { - [index: any]: string - } - export type SecondaryTestType = TestType - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType, SecondaryTestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([_key, value]) => (typeof value === "string")) - ) - } - - export function isSecondaryTestType(obj: unknown): obj is SecondaryTestType { - const typedObj = obj as SecondaryTestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([_key, value]) => (typeof value === "string")) - ) - } - `, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'adds type guard import to source file and also exports', - { - // NOTE: This file is not automatically cleaned up with `formatText` after - // being modified so it requires this funky indentation to ensure that it is - // conforms to ts-morph's formatting. - 'test.ts': ` -/** @see {isEmpty} ts-auto-guard:type-guard */ -export interface Empty { } -`, - }, - { - 'test.ts': ` - import * as CustomGuardAlias from "./test.guard"; - - /** @see {isEmpty} ts-auto-guard:type-guard */ - export interface Empty {} - export { CustomGuardAlias };`, - 'test.guard.ts': ` - import { Empty } from "./test"; - - export function isEmpty(obj: unknown): obj is Empty { - const typedObj = obj as Empty - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") - ) - }`, - }, - { options: { importGuards: 'CustomGuardAlias' } } -) - -testProcessProject( - 'imports and uses generated type guard if the type is used in another file', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - someKey: string | number - } - `, - 'test-list.ts': ` - import { TestType } from './test' - - /** @see {isTestTypeList} ts-auto-guard:type-guard */ - export type TestTypeList = Array - `, - }, - { - 'test.ts': null, - 'test-list.ts': null, - 'test-list.guard.ts': ` - import { isTestType } from "./test.guard"; - import { TestTypeList } from "./test-list"; - - export function isTestTypeList(obj: unknown): obj is TestTypeList { - const typedObj = obj as TestTypeList - return ( - Array.isArray(typedObj) && - typedObj.every((e: any) => - isTestType(e) as boolean - ) - ) - } - `, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - (typeof typedObj["someKey"] === "string" || - typeof typedObj["someKey"] === "number") - ) - } - `, - } -) - -testProcessProject( - 'generates type guards for dynamic object keys, including when mixed with static keys', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - someKey: "some" | "key" - [index: string]: "dynamic" | "string" - [index: number]: "also-dynamic" | "number" - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - (typedObj["someKey"] === "some" || - typedObj["someKey"] === "key") && - Object.entries(typedObj) - .filter(([key]) => !["someKey"].includes(key)) - .every(([key, value]) => ((value === "string" || - value === "dynamic") && - typeof key === "string" || - (value === "number" || - value === "also-dynamic") && - typeof key === "number")) - ) - } - `, - } -) - -testProcessProject( - 'generates type guards for Record types', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export type TestType = Record - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([key, value]) => ((value === "string" || - value === "dynamic") && - typeof key === "string")) - ) - } - `, - } -) - -testProcessProject( - 'prefixes value with underscore if it goes unused', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - [index: string]: any - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([key, _value]) => (typeof key === "string")) - ) - } - `, - } -) - -testProcessProject( - 'prefixes key with underscore if it goes unused', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - [index: any]: string - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([_key, value]) => (typeof value === "string")) - ) - } - `, - } -) - -testProcessProject( - 'Does not generate empty guard files', - { - 'test.ts': '', - }, - { 'test.ts': null } -) - -testProcessProject( - 'Deals with unknown type as it would any', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - [index: string]: unknown - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Object.entries(typedObj) - .every(([key, _value]) => (typeof key === "string")) - ) - } - `, - } -) - -testProcessProject( - 'Deals with unknown type as it would any', - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - test: unknown - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") - ) - } - `, - } -) - -testProcessProject( - 'Check if any callable properties is a function', - // should also emit a warning about how it is not possible to check function type at runtime. - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - test: (() => void) - // ts-auto-guard-suppress function-type - test2(someArg: number): boolean - // some other comments - test3: { - (someArg: string): number - test3Arg: number; - } - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["test"] === "function" && - typeof typedObj["test3"] === "function" && - typeof typedObj["test3"]["test3Arg"] === "number" && - typeof typedObj["test2"] === "function" - ) - } - `, - } -) - -testProcessProject( - 'Check if callable interface is a function', - // should also emit a warning about how it is not possible to check function type at runtime. - { - 'test.ts': ` - /** @see {isTestType} ts-auto-guard:type-guard */ - export interface TestType { - (someArg: string): number - arg: number; - } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { TestType } from "./test"; - - export function isTestType(obj: unknown): obj is TestType { - const typedObj = obj as TestType - return ( - typeof typedObj === "function" && - typeof typedObj["arg"] === "number" - ) - } - `, - } -) - -testProcessProject( - 'generated type guards for intersection type', - { - 'test.ts': ` - export type X = { foo: number } & { bar: string } - `, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { X } from "./test"; - - export function isX(obj: unknown): obj is X { - const typedObj = obj as X - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["foo"] === "number" && - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - typeof typedObj["bar"] === "string" - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'generates tuples', - { - 'test.ts': ` - export interface A { - b: [number] - }`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { A } from "./test"; - - export function isA(obj: unknown): obj is A { - const typedObj = obj as A - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - Array.isArray(typedObj["b"]) && - typeof typedObj["b"][0] === "number" - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'skips checking any type in array', - { - 'test.ts': `export type A = any[]`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { A } from "./test"; - - export function isA(obj: unknown): obj is A { - const typedObj = obj as A - return ( - Array.isArray(typedObj) - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'works for any type', - { - 'test.ts': `export type A = any`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { A } from "./test"; - - export function isA(obj: unknown): obj is A { - const typedObj = obj as A - return ( - true - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'works for unknown type', - { - 'test.ts': `export type A = unknown`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { A } from "./test"; - - export function isA(obj: unknown): obj is A { - const typedObj = obj as A - return ( - true - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'any and unknown work in union types', - { - 'test.ts': ` - type anyType = any - type unknownType = unknown - - export type AnyOrString = string | anyType - export type UnknownOrString = string | unknownType - export type AnyOrUnknownOrString = string | anyType | unknownType`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { AnyOrString, UnknownOrString, AnyOrUnknownOrString } from "./test"; - - export function isAnyOrString(obj: unknown): obj is AnyOrString { - const typedObj = obj as AnyOrString - return ( - true - ) - } - - export function isUnknownOrString(obj: unknown): obj is UnknownOrString { - const typedObj = obj as UnknownOrString - return ( - true - ) - } - - export function isAnyOrUnknownOrString(obj: unknown): obj is AnyOrUnknownOrString { - const typedObj = obj as AnyOrUnknownOrString - return ( - true - ) - }`, - }, - { options: { exportAll: true } } -) - -testProcessProject( - 'any and unknown work in interesction types', - { - 'test.ts': ` - type anyType = any - type unknownType = unknown - - export type AnyAndString = string & anyType - export type UnknownAndString = string & unknownType - export type AnyAndUnknownAndString = string & anyType & unknownType`, - }, - { - 'test.ts': null, - 'test.guard.ts': ` - import { AnyAndString, UnknownAndString, AnyAndUnknownAndString } from "./test"; - - export function isAnyAndString(obj: unknown): obj is AnyAndString { - const typedObj = obj as AnyAndString - return ( - true - ) - } - - export function isUnknownAndString(obj: unknown): obj is UnknownAndString { - const typedObj = obj as UnknownAndString - return ( - typeof typedObj === "string" - ) - } - - export function isAnyAndUnknownAndString(obj: unknown): obj is AnyAndUnknownAndString { - const typedObj = obj as AnyAndUnknownAndString - return ( - true - ) - }`, - }, - { options: { exportAll: true } } -) diff --git a/tests/import.ts b/tests/import.test.ts similarity index 100% rename from tests/import.ts rename to tests/import.test.ts diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..f9025fd --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDirs": ["../tests", "../src"] + } +} diff --git a/tsconfig.json b/tsconfig.json index 423a440..b96c905 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ "sourceMap": true /* Generates corresponding '.map' file. */, // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./lib" /* Redirect output structure to the directory. */, - "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + "rootDirs": [ + "./src" + ] /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */