diff --git a/.gitignore b/.gitignore index cd03923b..571f1f43 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ out /packages/*/test /packages/*/index.* +/packages/ts-simple-type/playground/**/* +/packages/ts-simple-type/.rollup.cache +!/packages/ts-simple-type/test + /packages/vscode-lit-plugin/built /packages/web-component-analyzer/.rollup.cache diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 553e917a..00000000 --- a/.prettierignore +++ /dev/null @@ -1,26 +0,0 @@ -.vscode -.idea -.editorconfig -node_modules -*.txt -*.tgz -*.log -.DS_Store -dist -out -*.vsix -*.gif -*.png -*.log -.eslintignore -.gitignore -.prettierignore -.vscodeignore - -/packages/*/lib -/packages/*/out -/packages/*/scripts -/packages/*/test -/packages/*/index.* - -/.nx/workspace-data \ No newline at end of file diff --git a/STATUS.md b/STATUS.md index 9e158b3d..da7dd40c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -26,7 +26,3 @@ The Lit team also have [this RFC](https://github.com/lit/rfcs/pull/41) outlining As a shoutout, some of the tools by break-stuff are great if you are wanting to generate a custom element manifest file. I would highly recommend checking them out: https://github.com/break-stuff/cem-tools - -## TS Simple Type - -Currently, this repo doesn't include the [ts-simple-type](https://github.com/runem/ts-simple-type) repo that was also created by Rune. There is a good chance it will be included here in the future, or an alternative will be found. If there is an update that you'd like to make to this package let me know, and I can see about getting it added here as well so that it is easier to do so! diff --git a/eslint.config.mjs b/eslint.config.mjs index 15e06ea4..d1a41bea 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,6 +19,8 @@ export default tseslint.config( "packages/*/scripts", "packages/*/test", "packages/*/index.*", + "packages/ts-simple-type/.rollup.cache", + "!packages/ts-simple-type/test", "packages/vscode-lit-plugin/built", "packages/web-component-analyzer/.rollup.cache", "!packages/web-component-analyzer/test", diff --git a/package-lock.json b/package-lock.json index 667ff43c..aa786875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "workspaces": [ "packages/lit-analyzer", "packages/ts-lit-plugin", + "packages/ts-simple-type", "packages/vscode-lit-plugin", "packages/web-component-analyzer" ], @@ -18,6 +19,7 @@ "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.0", "@eslint/js": "^9.24.0", + "@types/node": "^22.14.1", "@vscode/test-electron": "^2.3.8", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", @@ -1651,6 +1653,10 @@ "resolved": "packages/ts-lit-plugin", "link": true }, + "node_modules/@jackolope/ts-simple-type": { + "resolved": "packages/ts-simple-type", + "link": true + }, "node_modules/@jackolope/web-component-analyzer": { "resolved": "packages/web-component-analyzer", "link": true @@ -9949,10 +9955,6 @@ "node": ">=0.3.1" } }, - "node_modules/ts-simple-type": { - "version": "2.0.0-next.0", - "license": "MIT" - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "dev": true, @@ -10744,13 +10746,13 @@ "version": "3.1.2", "license": "MIT", "dependencies": { + "@jackolope/ts-simple-type": "^3.0.0", "@jackolope/web-component-analyzer": "^4.0.1", "@vscode/web-custom-data": "^0.5.0", "chalk": "^5.4.0", "didyoumean2": "7.0.4", "fast-glob": "^3.2.11", "parse5": "7.2.1", - "ts-simple-type": "~2.0.0-next.0", "vscode-css-languageservice": "6.3.4", "vscode-html-languageservice": "5.3.3" }, @@ -10758,7 +10760,6 @@ "lit-analyzer": "cli.mjs" }, "devDependencies": { - "@types/node": "^22.14.1", "ava": "^6.2.0", "tslib": "^2.0.0", "typescript": "^5.8.3", @@ -10782,7 +10783,6 @@ "@jackolope/web-component-analyzer": "^4.0.1" }, "devDependencies": { - "@types/node": "^22.14.1", "esbuild": "^0.25.1", "typescript": "^5.8.3", "wireit": "^0.14.11" @@ -10791,6 +10791,21 @@ "node": ">=18" } }, + "packages/ts-simple-type": { + "name": "@jackolope/ts-simple-type", + "version": "3.0.0", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.2", + "ava": "^6.2.0", + "rollup": "^4.39.0", + "ts-node": "^10.9.1", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "typescript": "^5" + } + }, "packages/vscode-lit-plugin": { "name": "lit-analyzer-plugin", "version": "2.2.0", @@ -10801,7 +10816,6 @@ "devDependencies": { "@jackolope/lit-analyzer": "^3.1.2", "@types/mocha": "^10.0.10", - "@types/node": "^22.14.1", "@types/vscode": "^1.30.0", "@vscode/vsce": "^3.3.2", "esbuild": "^0.25.1", @@ -10821,14 +10835,13 @@ "version": "4.0.1", "license": "MIT", "dependencies": { - "ts-simple-type": "2.0.0-next.0", + "@jackolope/ts-simple-type": "^3.0.0", "typescript": "^5.8.3" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-typescript": "^12.1.2", - "@types/node": "^22.14.1", "ava": "^6.2.0", "cross-env": "^7.0.2", "rollup": "^4.39.0", diff --git a/package.json b/package.json index 31107e00..2085969b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "workspaces": [ "packages/lit-analyzer", "packages/ts-lit-plugin", + "packages/ts-simple-type", "packages/vscode-lit-plugin", "packages/web-component-analyzer" ], @@ -32,7 +33,7 @@ "template" ], "scripts": { - "prettier:write": "prettier --write \"packages/*/src/**/*.ts\"", + "prettier:write": "prettier --write \"packages/**/*.ts\"", "readme": "wireit", "dev": "cd dev && TSS_DEBUG=5999 code . --disable-extension jackolope.lit-analyzer-plugin", "dev:logs": "touch dev/lit-plugin.log && tail -f dev/lit-plugin.log", @@ -53,6 +54,7 @@ "dependencies": [ "./packages/lit-analyzer:build", "./packages/ts-lit-plugin:build", + "./packages/ts-simple-type:build", "./packages/vscode-lit-plugin:build", "./packages/web-component-analyzer:build" ] @@ -72,6 +74,7 @@ "test:headless": { "dependencies": [ "./packages/lit-analyzer:test", + "./packages/ts-simple-type:test", "./packages/web-component-analyzer:test:all" ] }, @@ -94,6 +97,7 @@ "dependencies": [ "./packages/lit-analyzer:eslint", "./packages/ts-lit-plugin:eslint", + "./packages/ts-simple-type:eslint", "./packages/vscode-lit-plugin:eslint", "./packages/web-component-analyzer:eslint" ] @@ -103,7 +107,7 @@ "packages/*/src/**/*.ts", "prettier.config.js" ], - "command": "prettier --check \"packages/*/src/**/*.ts\"" + "command": "prettier --check \"packages/**/*.ts\"" }, "package": { "dependencies": [ @@ -123,6 +127,7 @@ "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.0", "@eslint/js": "^9.24.0", + "@types/node": "^22.14.1", "@vscode/test-electron": "^2.3.8", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", diff --git a/packages/lit-analyzer/package.json b/packages/lit-analyzer/package.json index 66fe7476..1a464703 100644 --- a/packages/lit-analyzer/package.json +++ b/packages/lit-analyzer/package.json @@ -2,7 +2,7 @@ "name": "@jackolope/lit-analyzer", "version": "3.1.2", "description": "CLI that type checks bindings in lit-html templates", - "author": "runem", + "author": "JackRobards", "license": "MIT", "repository": { "type": "git", @@ -36,6 +36,10 @@ }, "wireit": { "build": { + "dependencies": [ + "../ts-simple-type:build", + "../web-component-analyzer:build" + ], "command": "tsc --build --pretty", "files": [ "src/**/*", @@ -90,18 +94,17 @@ "cli.mjs" ], "dependencies": { + "@jackolope/ts-simple-type": "^3.0.0", + "@jackolope/web-component-analyzer": "^4.0.1", "@vscode/web-custom-data": "^0.5.0", "chalk": "^5.4.0", "didyoumean2": "7.0.4", "fast-glob": "^3.2.11", "parse5": "7.2.1", - "ts-simple-type": "~2.0.0-next.0", "vscode-css-languageservice": "6.3.4", - "vscode-html-languageservice": "5.3.3", - "@jackolope/web-component-analyzer": "^4.0.1" + "vscode-html-languageservice": "5.3.3" }, "devDependencies": { - "@types/node": "^22.14.1", "ava": "^6.2.0", "tslib": "^2.0.0", "typescript": "^5.8.3", diff --git a/packages/lit-analyzer/src/lib/analyze/data/extra-html-data.ts b/packages/lit-analyzer/src/lib/analyze/data/extra-html-data.ts index 8f772a91..a30fc77b 100644 --- a/packages/lit-analyzer/src/lib/analyze/data/extra-html-data.ts +++ b/packages/lit-analyzer/src/lib/analyze/data/extra-html-data.ts @@ -1,4 +1,4 @@ -import type { SimpleType, SimpleTypeStringLiteral, SimpleTypeUnion } from "ts-simple-type"; +import type { SimpleType, SimpleTypeStringLiteral, SimpleTypeUnion } from "@jackolope/ts-simple-type"; import { makePrimitiveArrayType } from "../util/type-util.js"; const HTML_5_ATTR_TYPES: { [key: string]: string | string[] | [string[]] } = { diff --git a/packages/lit-analyzer/src/lib/analyze/data/get-built-in-html-collection.ts b/packages/lit-analyzer/src/lib/analyze/data/get-built-in-html-collection.ts index bd2cd2fa..a53bfcfc 100644 --- a/packages/lit-analyzer/src/lib/analyze/data/get-built-in-html-collection.ts +++ b/packages/lit-analyzer/src/lib/analyze/data/get-built-in-html-collection.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { HTMLDataV1 } from "vscode-html-languageservice"; import htmlDataJson from "@vscode/web-custom-data/data/browsers.html-data.json"; import type { HtmlAttr, HtmlDataCollection } from "../parse/parse-html-data/html-tag.js"; diff --git a/packages/lit-analyzer/src/lib/analyze/data/get-user-config-html-collection.ts b/packages/lit-analyzer/src/lib/analyze/data/get-user-config-html-collection.ts index 3034c172..f83f8479 100644 --- a/packages/lit-analyzer/src/lib/analyze/data/get-user-config-html-collection.ts +++ b/packages/lit-analyzer/src/lib/analyze/data/get-user-config-html-collection.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from "fs"; -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { HTMLDataV1 } from "vscode-html-languageservice"; import type { LitAnalyzerConfig } from "../lit-analyzer-config.js"; import type { HtmlAttr, HtmlDataCollection, HtmlEvent, HtmlTag } from "../parse/parse-html-data/html-tag.js"; diff --git a/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts b/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts index 01385125..2224a495 100644 --- a/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts +++ b/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attr-values.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { isSimpleTypeLiteral } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isSimpleTypeLiteral } from "@jackolope/ts-simple-type"; import type { LitAnalyzerContext } from "../../../lit-analyzer-context.js"; import { HtmlNodeAttrAssignmentKind } from "../../../types/html-node/html-node-attr-assignment-types.js"; import type { HtmlNodeAttr } from "../../../types/html-node/html-node-attr-types.js"; diff --git a/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts b/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts index ff5ada52..8f872b1a 100644 --- a/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts +++ b/packages/lit-analyzer/src/lib/analyze/document-analyzer/html/completion/completions-for-html-attrs.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { isAssignableToSimpleTypeKind } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind } from "@jackolope/ts-simple-type"; import { LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER, LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER, diff --git a/packages/lit-analyzer/src/lib/analyze/parse/convert-component-definitions-to-html-collection.ts b/packages/lit-analyzer/src/lib/analyze/parse/convert-component-definitions-to-html-collection.ts index 8717607c..fea6e847 100644 --- a/packages/lit-analyzer/src/lib/analyze/parse/convert-component-definitions-to-html-collection.ts +++ b/packages/lit-analyzer/src/lib/analyze/parse/convert-component-definitions-to-html-collection.ts @@ -1,5 +1,5 @@ -import type { SimpleType, SimpleTypeAny } from "ts-simple-type"; -import { isSimpleType, toSimpleType } from "ts-simple-type"; +import type { SimpleType, SimpleTypeAny } from "@jackolope/ts-simple-type"; +import { isSimpleType, toSimpleType } from "@jackolope/ts-simple-type"; import type { TypeChecker } from "typescript"; import type { AnalyzerResult, ComponentDeclaration, ComponentDefinition, ComponentFeatures } from "@jackolope/web-component-analyzer"; import { lazy } from "../util/general-util.js"; diff --git a/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/html-tag.ts b/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/html-tag.ts index 3a064489..f1648125 100644 --- a/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/html-tag.ts +++ b/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/html-tag.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { isAssignableToSimpleTypeKind, typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind, typeToString } from "@jackolope/ts-simple-type"; import type { ComponentCssPart, ComponentCssProperty, diff --git a/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/parse-vscode-html-data.ts b/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/parse-vscode-html-data.ts index bacf4bfe..bd9b8264 100644 --- a/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/parse-vscode-html-data.ts +++ b/packages/lit-analyzer/src/lib/analyze/parse/parse-html-data/parse-vscode-html-data.ts @@ -1,4 +1,4 @@ -import type { SimpleType, SimpleTypeStringLiteral } from "ts-simple-type"; +import type { SimpleType, SimpleTypeStringLiteral } from "@jackolope/ts-simple-type"; import type { HTMLDataV1, IAttributeData, ITagData, IValueData, IValueSet } from "vscode-html-languageservice"; import type { MarkupContent } from "vscode-languageserver-types"; import { lazy } from "../../util/general-util.js"; diff --git a/packages/lit-analyzer/src/lib/analyze/store/html-store/html-data-source-merged.ts b/packages/lit-analyzer/src/lib/analyze/store/html-store/html-data-source-merged.ts index 574e91b0..0166545e 100644 --- a/packages/lit-analyzer/src/lib/analyze/store/html-store/html-data-source-merged.ts +++ b/packages/lit-analyzer/src/lib/analyze/store/html-store/html-data-source-merged.ts @@ -1,4 +1,4 @@ -import type { SimpleType, SimpleTypeUnion } from "ts-simple-type"; +import type { SimpleType, SimpleTypeUnion } from "@jackolope/ts-simple-type"; import type { HtmlAttr, HtmlCssPart, diff --git a/packages/lit-analyzer/src/lib/analyze/ts-module.ts b/packages/lit-analyzer/src/lib/analyze/ts-module.ts index 2e3d9d8e..7b30b5dd 100644 --- a/packages/lit-analyzer/src/lib/analyze/ts-module.ts +++ b/packages/lit-analyzer/src/lib/analyze/ts-module.ts @@ -1,4 +1,4 @@ -import { setTypescriptModule as tsSimpleTypeSetTypescriptModule } from "ts-simple-type"; +import { setTypescriptModule as tsSimpleTypeSetTypescriptModule } from "@jackolope/ts-simple-type"; import * as tsModuleType from "typescript"; export const tsModule: { ts: typeof tsModuleType } = { ts: tsModuleType }; diff --git a/packages/lit-analyzer/src/lib/analyze/util/type-util.ts b/packages/lit-analyzer/src/lib/analyze/util/type-util.ts index bfe34c15..42e832d9 100644 --- a/packages/lit-analyzer/src/lib/analyze/util/type-util.ts +++ b/packages/lit-analyzer/src/lib/analyze/util/type-util.ts @@ -1,4 +1,4 @@ -import type { SimpleType, SimpleTypeUnion } from "ts-simple-type"; +import type { SimpleType, SimpleTypeUnion } from "@jackolope/ts-simple-type"; const PRIMITIVE_STRING_ARRAY_TYPE_BRAND = Symbol("PRIMITIVE_STRING_ARRAY_TYPE"); diff --git a/packages/lit-analyzer/src/lib/rules/no-boolean-in-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/no-boolean-in-attribute-binding.ts index 44253285..22b256ad 100644 --- a/packages/lit-analyzer/src/lib/rules/no-boolean-in-attribute-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/no-boolean-in-attribute-binding.ts @@ -1,4 +1,4 @@ -import { isAssignableToSimpleTypeKind } from "ts-simple-type"; +import { isAssignableToSimpleTypeKind } from "@jackolope/ts-simple-type"; import { LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER } from "../analyze/constants.js"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; diff --git a/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts index c9195906..8c2dc08b 100644 --- a/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts @@ -1,4 +1,4 @@ -import { isAssignableToPrimitiveType, typeToString } from "ts-simple-type"; +import { isAssignableToPrimitiveType, typeToString } from "@jackolope/ts-simple-type"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModule } from "../analyze/types/rule/rule-module.js"; diff --git a/packages/lit-analyzer/src/lib/rules/no-incompatible-property-type.ts b/packages/lit-analyzer/src/lib/rules/no-incompatible-property-type.ts index db19845d..dc64faf4 100644 --- a/packages/lit-analyzer/src/lib/rules/no-incompatible-property-type.ts +++ b/packages/lit-analyzer/src/lib/rules/no-incompatible-property-type.ts @@ -1,5 +1,5 @@ -import type { SimpleType, SimpleTypeKind } from "ts-simple-type"; -import { isAssignableToSimpleTypeKind, isSimpleType, toSimpleType, typeToString } from "ts-simple-type"; +import type { SimpleType, SimpleTypeKind } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind, isSimpleType, toSimpleType, typeToString } from "@jackolope/ts-simple-type"; import type { Node } from "typescript"; import type { LitElementPropertyConfig } from "@jackolope/web-component-analyzer"; import type { RuleModule } from "../analyze/types/rule/rule-module.js"; diff --git a/packages/lit-analyzer/src/lib/rules/no-invalid-directive-binding.ts b/packages/lit-analyzer/src/lib/rules/no-invalid-directive-binding.ts index 3846baa7..2f442683 100644 --- a/packages/lit-analyzer/src/lib/rules/no-invalid-directive-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/no-invalid-directive-binding.ts @@ -1,4 +1,4 @@ -import { isAssignableToType } from "ts-simple-type"; +import { isAssignableToType } from "@jackolope/ts-simple-type"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModule } from "../analyze/types/rule/rule-module.js"; diff --git a/packages/lit-analyzer/src/lib/rules/no-noncallable-event-binding.ts b/packages/lit-analyzer/src/lib/rules/no-noncallable-event-binding.ts index 3aaf0b31..7f189f76 100644 --- a/packages/lit-analyzer/src/lib/rules/no-noncallable-event-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/no-noncallable-event-binding.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { isAssignableToSimpleTypeKind, typeToString, validateType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind, typeToString, validateType } from "@jackolope/ts-simple-type"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModule } from "../analyze/types/rule/rule-module.js"; import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util.js"; diff --git a/packages/lit-analyzer/src/lib/rules/no-nullable-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/no-nullable-attribute-binding.ts index 6e37c216..ab044714 100644 --- a/packages/lit-analyzer/src/lib/rules/no-nullable-attribute-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/no-nullable-attribute-binding.ts @@ -1,4 +1,4 @@ -import { isAssignableToSimpleTypeKind, typeToString } from "ts-simple-type"; +import { isAssignableToSimpleTypeKind, typeToString } from "@jackolope/ts-simple-type"; import { HtmlNodeAttrAssignmentKind } from "../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModule } from "../analyze/types/rule/rule-module.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/directive/get-directive.ts b/packages/lit-analyzer/src/lib/rules/util/directive/get-directive.ts index a39e1647..0d8477c4 100644 --- a/packages/lit-analyzer/src/lib/rules/util/directive/get-directive.ts +++ b/packages/lit-analyzer/src/lib/rules/util/directive/get-directive.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { toSimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { toSimpleType } from "@jackolope/ts-simple-type"; import type { Expression } from "typescript"; import type { HtmlNodeAttrAssignment } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/directive/is-lit-directive.ts b/packages/lit-analyzer/src/lib/rules/util/directive/is-lit-directive.ts index f50ba68d..30c3efe2 100644 --- a/packages/lit-analyzer/src/lib/rules/util/directive/is-lit-directive.ts +++ b/packages/lit-analyzer/src/lib/rules/util/directive/is-lit-directive.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; const partTypeNames: ReadonlySet = new Set([ "Part", diff --git a/packages/lit-analyzer/src/lib/rules/util/type/extract-binding-types.ts b/packages/lit-analyzer/src/lib/rules/util/type/extract-binding-types.ts index de631aa0..0fd2726d 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/extract-binding-types.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/extract-binding-types.ts @@ -1,5 +1,11 @@ -import type { SimpleType, SimpleTypeBooleanLiteral, SimpleTypeEnumMember, SimpleTypeString, SimpleTypeStringLiteral } from "ts-simple-type"; -import { isSimpleType, toSimpleType } from "ts-simple-type"; +import type { + SimpleType, + SimpleTypeBooleanLiteral, + SimpleTypeEnumMember, + SimpleTypeString, + SimpleTypeStringLiteral +} from "@jackolope/ts-simple-type"; +import { isSimpleType, toSimpleType } from "@jackolope/ts-simple-type"; import type { Expression, Type, TypeChecker } from "typescript"; import type { HtmlNodeAttrAssignment } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-binding-under-security-system.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-binding-under-security-system.ts index 38f1737d..c18e65f0 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-binding-under-security-system.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-binding-under-security-system.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import type { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; import { isLitDirective } from "../directive/is-lit-directive.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts index 896c2f6e..b43b729c 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts @@ -1,5 +1,5 @@ -import type { SimpleType, SimpleTypeComparisonOptions } from "ts-simple-type"; -import { isAssignableToType as _isAssignableToType, typeToString } from "ts-simple-type"; +import type { SimpleType, SimpleTypeComparisonOptions } from "@jackolope/ts-simple-type"; +import { isAssignableToType as _isAssignableToType, typeToString } from "@jackolope/ts-simple-type"; import type { HtmlNodeAttrAssignment } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; import { HtmlNodeAttrAssignmentKind } from "../../../analyze/types/html-node/html-node-attr-assignment-types.js"; import type { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-boolean-binding.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-boolean-binding.ts index 5acb3a63..71656c9f 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-boolean-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-boolean-binding.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import type { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-element-binding.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-element-binding.ts index 4da5247f..93049d12 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-element-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-element-binding.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import type { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-property-binding.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-property-binding.ts index 7f4a4d9e..d96874ee 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-property-binding.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-property-binding.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import type { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types.js"; import type { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util.js"; diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-to-type.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-to-type.ts index 7cd1ad03..f7a4980f 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-to-type.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-to-type.ts @@ -1,5 +1,5 @@ -import type { SimpleType, SimpleTypeComparisonOptions } from "ts-simple-type"; -import { isAssignableToType as _isAssignableToType } from "ts-simple-type"; +import type { SimpleType, SimpleTypeComparisonOptions } from "@jackolope/ts-simple-type"; +import { isAssignableToType as _isAssignableToType } from "@jackolope/ts-simple-type"; import type { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context.js"; export function isAssignableToType( diff --git a/packages/lit-analyzer/src/lib/rules/util/type/remove-undefined-from-type.ts b/packages/lit-analyzer/src/lib/rules/util/type/remove-undefined-from-type.ts index ebf2ff09..058dc5db 100644 --- a/packages/lit-analyzer/src/lib/rules/util/type/remove-undefined-from-type.ts +++ b/packages/lit-analyzer/src/lib/rules/util/type/remove-undefined-from-type.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { isAssignableToSimpleTypeKind } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind } from "@jackolope/ts-simple-type"; export function removeUndefinedFromType(type: SimpleType): SimpleType { switch (type.kind) { diff --git a/packages/ts-lit-plugin/package.json b/packages/ts-lit-plugin/package.json index 6d3f9b0d..752c01fb 100644 --- a/packages/ts-lit-plugin/package.json +++ b/packages/ts-lit-plugin/package.json @@ -30,7 +30,8 @@ "wireit": { "build": { "dependencies": [ - "../lit-analyzer:build" + "../lit-analyzer:build", + "../web-component-analyzer:build" ], "command": "tsc --build --pretty", "files": [ @@ -59,7 +60,6 @@ "@jackolope/web-component-analyzer": "^4.0.1" }, "devDependencies": { - "@types/node": "^22.14.1", "esbuild": "^0.25.1", "typescript": "^5.8.3", "wireit": "^0.14.11" diff --git a/packages/ts-simple-type/CHANGELOG.md b/packages/ts-simple-type/CHANGELOG.md new file mode 100644 index 00000000..193ef20c --- /dev/null +++ b/packages/ts-simple-type/CHANGELOG.md @@ -0,0 +1,208 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [1.0.0] - 2020-07-04 + +### Breaking Changes + +- `toTypeString` has been renamed to `typeToString`. +- `simpleTypeToString` is no longer exported. +- `spread` on `SimpleTypeFunctionParameter` has been renamed to `rest`. +- `hasRestElement` on `SimpleTypeTuple` has been renamed to `rest`. +- `SimpleTypeKind` and `SimpleTypeModifierKind` have been converted to string literal unions.. +- `methods` and `properties` on `SimpleTypeClass` have been renamed to `members`. +- `argTypes` on `SimpleTypeFunction` and `SimpleTypeMethod` have been renamed to `parameters`. +- `CIRCULAR_REF` SimpleType has been removed. +- `SimpleTypeFunctionArgument` has been renamed to `SimpleTypeFunctionParameter`. + +### Bug Fixes + +- Added support for "Object", "Number", "Boolean", "BigInt", "String". +- Improved type checking support for intersection types. +- Fixed type checking of function type rest parameters. +- `optional` is now added properly to class members. +- Improved type checking of functions/methods. +- Improved type checking of class/interface/object. +- Type parameters now default to `unknown` instead of `any`. +- Members with call signatures are now `methods` instead of `functions`. + +### Features + +- All `SimpleType`'s are now lazy per default. Types will evaluate when interacting with the object. This behavior can be overwritten with `{eager: true}`. +- Added helper functions to serialize and deserialize types making it possible to store types with circular references. +- Added new SimpleTypeKind "NON_PRIMITIVE" representing the non-primitive type: `object`. +- Added new SimpleTypeKidn "ES_SYMBOL" and "ES_SYMBOL_UNIQUE" representing the `Symbol` type. +- Added support for type checking **constructors** and **call signatures** on object types. +- Added `validateType` function that makes it possible easily make custom validator functions with `SimpleType`. +- The results of converting Type to SimpleType are now always cached and used whenever calling `toSimpleType`. It's possible to supply this function with your own cache. +- The results of checking type assignability are now always cached and used whenever calling `isAssignableToType`. It's possible to supply this function with your own cache. +- Added `serializeSimpleType` and `deserializeSimpleType` functions. +- All members of `SimpleType` are now `readonly`. +- If two `ts.Type` values are given to `isAssignableToType`, the function will prioritize testing using `isTypeAssignableTo` on the type checker if it has been exposed. + +### Project + +- Updated all dependencies. +- Cleaned up project structure. +- Added script to quickly test and debug assignability (`npm run playground`). + +## [0.3.7] - 2019-11-08 + +### Fixed + +- Fix breaking API changes in Typescript 3.7 ([741c837e](https://github.com/runem/ts-simple-type/commit/741c837e4a915fcb42526b5fa5551e7002b8a6e0)) +- Relax check in extendTypeParameterMap ([f5da8437](https://github.com/runem/ts-simple-type/commit/f5da8437afe95ce2d05c21f854ab201761832d68)) +- Add 'void' to 'PRIMITIVE_TYPE_KINDS' because it represents 'undefined' ([ad5c7bcf](https://github.com/runem/ts-simple-type/commit/ad5c7bcf894097f670e13090f1c11114961d5736)) + +## [0.3.6](https://github.com/runem/ts-simple-type/compare/v0.3.5...v0.3.6) (2019-08-17) + +### Features + +- Make it possible to overwrite type checking logic by running user defined code when comparing types ([5a5e376](https://github.com/runem/ts-simple-type/commit/5a5e376)) + + + +## [0.3.5](https://github.com/runem/ts-simple-type/compare/v0.3.4...v0.3.5) (2019-07-16) + +### Bug Fixes + +- Check in rollup.config.js. This fixes [#6](https://github.com/runem/ts-simple-type/issues/6) ([5fcb5cc](https://github.com/runem/ts-simple-type/commit/5fcb5cc)) +- Fix multiple failing assignability checks when comparing tuples, intersections, never and object types ([b1b06c0](https://github.com/runem/ts-simple-type/commit/b1b06c0)) +- Fix some failing tests when comparing recursive types and object assignability with zero properties in common ([11ca879](https://github.com/runem/ts-simple-type/commit/11ca879)) +- Fix tuple and intersection checking ([95bdcdb](https://github.com/runem/ts-simple-type/commit/95bdcdb)) +- Fix typo 'SimpleTyoeCircularRef'. This closes [#7](https://github.com/runem/ts-simple-type/issues/7) ([5b73bf1](https://github.com/runem/ts-simple-type/commit/5b73bf1)) + + + +## [0.3.3](https://github.com/runem/ts-simple-type/compare/v0.3.2...v0.3.3) (2019-05-02) + + + +## [0.3.2](https://github.com/runem/ts-simple-type/compare/v0.3.1...v0.3.2) (2019-04-26) + + + +## [0.3.1](https://github.com/runem/ts-simple-type/compare/v0.2.28...v0.3.1) (2019-04-25) + +### Bug Fixes + +- Fix generic type recursion ([e65b106](https://github.com/runem/ts-simple-type/commit/e65b106)) + +### Features + +- Add support for intersection types and never types ([f7d531b](https://github.com/runem/ts-simple-type/commit/f7d531b)) + + + +# [0.3.0](https://github.com/runem/ts-simple-type/compare/v0.2.28...v0.3.0) (2019-04-23) + +### Features + +- Add support for intersection types and never types ([f7d531b](https://github.com/runem/ts-simple-type/commit/f7d531b)) +- Add support for strict null checks ([19fce94](https://github.com/runem/ts-simple-type/commit/19fce94e869ff1f125764f5d102cda617373d563)) + + + +## [0.2.28](https://github.com/runem/ts-simple-type/compare/v0.2.27...v0.2.28) (2019-04-08) + +### Bug Fixes + +- ArrayLike and PromiseLike ([6d51122](https://github.com/runem/ts-simple-type/commit/6d51122)) +- Fix various function checks and add more function related test cases ([cd8c1c5](https://github.com/runem/ts-simple-type/commit/cd8c1c5)) + + + +## [0.2.27](https://github.com/runem/ts-simple-type/compare/v0.2.26...v0.2.27) (2019-03-07) + +### Bug Fixes + +- Fix problem where isAssignableToSimpleTypeKind wouldn't match ANY ([7edd4b3](https://github.com/runem/ts-simple-type/commit/7edd4b3)) + + + +## [0.2.26](https://github.com/runem/ts-simple-type/compare/v0.2.24...v0.2.26) (2019-03-07) + +### Features + +- isAssignableToSimpleTypeKind now treats kind OBJECT without members as kind ANY ([b75ff9a](https://github.com/runem/ts-simple-type/commit/b75ff9a)) + + + +## [0.2.25](https://github.com/runem/ts-simple-type/compare/v0.2.24...v0.2.25) (2019-02-25) + + + +## [0.2.24](https://github.com/runem/ts-simple-type/compare/v0.2.23...v0.2.24) (2019-02-25) + +### Bug Fixes + +- Allow assigning anything but 'null' and 'undefined' to the type '{}' ([5f0b097](https://github.com/runem/ts-simple-type/commit/5f0b097)) + + + +## [0.2.23](https://github.com/runem/ts-simple-type/compare/v0.2.22...v0.2.23) (2019-02-15) + +### Bug Fixes + +- Issue where isAssignableToSimpleTypeKind would fail with type 'ANY' ([38d7743](https://github.com/runem/ts-simple-type/commit/38d7743)) + + + +## [0.2.22](https://github.com/runem/ts-simple-type/compare/v0.2.21...v0.2.22) (2019-02-15) + + + +## [0.2.21](https://github.com/runem/ts-simple-type/compare/v0.2.20...v0.2.21) (2019-02-15) + +### Features + +- Add function that can return the string representation of either a native typescript type or a simple type ([2019248](https://github.com/runem/ts-simple-type/commit/2019248)) + + + +## [0.2.20](https://github.com/runem/ts-simple-type/compare/v0.2.19...v0.2.20) (2019-02-12) + +### Bug Fixes + +- Fix problem where recursive types created from the cache would crash the type checking ([b62167a](https://github.com/runem/ts-simple-type/commit/b62167a)) + + + +## [0.2.19](https://github.com/runem/ts-simple-type/compare/v0.2.18...v0.2.19) (2019-02-11) + +### Features + +- Add 'Date' type for performance gains. ([a8c74de](https://github.com/runem/ts-simple-type/commit/a8c74de)) + + + +## [0.2.18](https://github.com/runem/ts-simple-type/compare/v0.2.17...v0.2.18) (2019-02-10) + +### Features + +- Add ALIAS, GENERIC and PROMISE types. Refactor and improve type checking logic especially for very complex types. ([e1e636c](https://github.com/runem/ts-simple-type/commit/e1e636c)) + + + +## [0.2.17](https://github.com/runem/ts-simple-type/compare/v0.2.16...v0.2.17) (2019-01-15) + +### Bug Fixes + +- Fix function that checks if input to functions is node or type ([3eafb07](https://github.com/runem/ts-simple-type/commit/3eafb07)) + + + +## 0.2.16 (2019-01-10) + +### Features + +- Add support for circular referenced types ([90ba8f5](https://github.com/runem/ts-simple-type/commit/90ba8f5)) + + + +## 0.2.15 (2019-01-10) + +### Features + +- Add support for circular referenced types ([90ba8f5](https://github.com/runem/ts-simple-type/commit/90ba8f5)) diff --git a/packages/ts-simple-type/LICENSE b/packages/ts-simple-type/LICENSE new file mode 100644 index 00000000..4610ebc2 --- /dev/null +++ b/packages/ts-simple-type/LICENSE @@ -0,0 +1,7 @@ +Copyright 2019 Rune Mehlsen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/ts-simple-type/README.md b/packages/ts-simple-type/README.md new file mode 100644 index 00000000..129f9d79 --- /dev/null +++ b/packages/ts-simple-type/README.md @@ -0,0 +1,156 @@ +# ts-simple-type + +Downloads per month +NPM Version +Contributors +MIT License + +## What is this? + +Fork of the original `ts-simple-type`. + +Right now the type checker for Typescript API doesn't expose methods for checking assignability and building types. See issue [#9879](https://github.com/Microsoft/TypeScript/issues/9879) and [#29432](https://github.com/Microsoft/TypeScript/issues/29432) on the Typescript github repository. + +To fill in the gap while this issue is being discussed this library aims to provide the most essential helper functions for working with types in Typescript. + +Furthermore, this library can help you construct types (called `SimpleType`) which can be serialized and easy analyzed. + +This library has more than 35000 tests comparing results to actual Typescript diagnostics (see [test-types.ts](https://github.com/runem/ts-simple-type/blob/master/test-types/test-types.ts)). + +## Installation + +```bash +npm install @jackolope/ts-simple-type +``` + +## How to use + +The API is very simple. For example if you want to check if Typescript type `typeB` is assignable to `typeA`, you can use the following function. + +```typescript +import { isAssignableToType } from "@jackolope/ts-simple-type"; + +const isAssignable = isAssignableToType(typeA, typeB, typeChecker); +``` + +## SimpleType + +To make it easier to work with typescript types this library works by (behind the curtain) converting them to the interface `SimpleType`. Most functions in this library work with both `SimpleType` and the known and loved Typescript-provided `Type` interface. This means that you can easily create a complex type yourself and compare it to a native Typescript type. It also means that you can use this library to serialize types and even compare them in the browser. + +The `SimpleType` interface can be used to construct your own types for typechecking. + +```typescript +import { SimpleType, typeToString, isAssignableToType, isAssignableToValue } from "@jackolope/ts-simple-type"; + +const colors: SimpleType = { + kind: "UNION", + types: [ + { kind: "STRING_LITERAL", value: "RED" }, + { kind: "STRING_LITERAL", value: "GREEN" }, + { kind: "STRING_LITERAL", value: "BLUE" } + ] +}; + +(typeToString(colors) > "RED") | "GREEN" | "BLUE"; + +isAssignableToType(colors, { kind: "STRING_LITERAL", value: "YELLOW" }) > false; + +isAssignableToValue(colors, "BLUE") > true; + +isAssignableToValue(colors, "PINK") > false; +``` + +## More examples + +```typescript +const typeA = checker.getTypeAtLocation(nodeA); +const typeB = checker.getTypeAtLocation(nodeB); + +/* + For this example, let's say: + - typeA is number + - typeB is string[] +*/ + +// typeToString +typeToString(typeA) > "number"; + +typeToString(typeB) > "string[]"; + +// isAssignableToType +isAssignableToType(typeA, typeB, checker) > false; + +isAssignableToType(typeA, { kind: "NUMBER" }, checker) > true; + +isAssignableToType(typeB, { kind: "ARRAY", type: { kind: "STRING" } }, checker) > true; + +isAssignableToType({ kind: "STRING" }, { kind: "STRING_LITERAL", value: "hello" }) > true; + +// isAssignableToPrimitiveType +isAssignableToPrimitiveType(typeA, checker) > true; + +isAssignableToPrimitiveType(typeB, checker) > false; + +isAssignableToPrimitiveType({ kind: "ARRAY", type: { kind: "STRING" } }) > false; + +// isAssignableToSimpleTypeKind +isAssignableToSimpleTypeKind(typeA, "NUMBER", checker) > true; + +isAssignableToSimpleTypeKind(typeB, "BOOLEAN", checker) > false; + +isAssignableToSimpleTypeKind(typeB, ["STRING", "UNDEFINED"], checker) > true; + +// isAssignableToValue +isAssignableToValue(typeA, 123, checker) > true; + +isAssignableToValue(typeA, "hello", checker) > false; + +isAssignableToValue(typeB, true, checker) > false; + +// toSimpleType +toSimpleType(typeA, { checker }) > { kind: "NUMBER" }; + +toSimpleType(typeB, { checker }) > { kind: "ARRAY", type: { kind: "NUMBER" } }; +``` + +## API Documentation + +For functions that take either a native Typescript `Type` or a `SimpleType` the `TypeChecker` is only required if a Typescript `Type` has been given to the function. + +### isAssignableToType + +> isAssignableToType(typeA: Type | SimpleType, typeB: Type | SimpleType, checker?: TypeChecker): boolean + +Returns true if `typeB` is assignable to `typeA`. + +### isAssignableToPrimitiveType + +> isAssignableToPrimitiveType(type: Type | SimpleType, checker?: TypeChecker): boolean + +Returns true if `type` is assignable to a primitive type like `string`, `number`, `boolean`, `bigint`, `null` or `undefined`. + +### isAssignableToSimpleTypeKind + +> isAssignableToSimpleTypeKind(type: Type | SimpleType, kind: SimpleTypeKind | SimpleTypeKind[], checker?: TypeChecker, options?: Options): boolean + +Returns true if `type` is assignable to a `SimpleTypeKind`. + +- `options.matchAny` (boolean): Can be used to allow the "any" type to match everything. + +### isAssignableToValue + +> isAssignableToValue(type: SimpleType | Type, value: any, checker?: TypeChecker): boolean + +Returns true if the type of the value is assignable to `type`. + +### typeToString + +> typeToString(type: SimpleType): string + +Returns a string representation of the simple type. The string representation matches the one that Typescript generates. + +### toSimpleType + +> toSimpleType(type: Type | Node, checker: TypeChecker): SimpleType + +Returns a `SimpleType` that represents a native Typescript `Type`. diff --git a/packages/ts-simple-type/assignments.md b/packages/ts-simple-type/assignments.md new file mode 100644 index 00000000..60dd0ed7 --- /dev/null +++ b/packages/ts-simple-type/assignments.md @@ -0,0 +1,47 @@ +# Assignments + +This table illustrates which types can be assigned to each other. + +Each cell shows if the assignment `typeA = typeB` is valid. + + ## Assignments with strict options: + +| typeB ➡️
typeA ⬇️ | true | false | boolean | 123 | number | "foo" | string | undefined | null | 111n | BigInt | never | void | any | unknown | {} | +| ---------------------- | ---- | ----- | ------- | --- | ------ | ----- | ------ | --------- | ---- | ---- | ------ | ----- | ---- | --- | ------- | --- | +| true | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| false | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| boolean | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| 123 | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| number | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| "foo" | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| string | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| undefined | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| null | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| 111n | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| BigInt | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| never | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| void | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | +| any | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| unknown | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| {} | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | + + ## Assignments with non-strict options: + +| typeB ➡️
typeA ⬇️ | true | false | boolean | 123 | number | "foo" | string | undefined | null | 111n | BigInt | never | void | any | unknown | {} | +| ---------------------- | ---- | ----- | ------- | --- | ------ | ----- | ------ | --------- | ---- | ---- | ------ | ----- | ---- | --- | ------- | --- | +| true | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| false | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| boolean | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| 123 | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| number | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| "foo" | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| string | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| undefined | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| null | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| 111n | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| BigInt | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| never | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| void | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | +| any | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| unknown | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| {} | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | diff --git a/packages/ts-simple-type/package.json b/packages/ts-simple-type/package.json new file mode 100644 index 00000000..93f93b7a --- /dev/null +++ b/packages/ts-simple-type/package.json @@ -0,0 +1,101 @@ +{ + "name": "@jackolope/ts-simple-type", + "version": "3.0.0", + "description": "Relationship type checker functions for Typescript types. Fork of the original ts-simple-type", + "author": "JackRobards", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/JackRobards/lit-analyzer.git" + }, + "keywords": [ + "typescript", + "ast", + "typechecker", + "type" + ], + "scripts": { + "playground": "ts-node run-playground.ts", + "test": "wireit", + "test:watch": "ava --color --watch", + "build": "wireit", + "watch": "rollup -c --watch", + "eslint": "eslint src test", + "prettier:check": "prettier --check \"src/**/*.{ts,tsx}\"", + "prettier:write": "prettier --write \"src/**/*.{ts,tsx}\"" + }, + "wireit": { + "build": { + "dependencies": [ + "build:types" + ], + "command": "rollup -c", + "files": [ + "src/**/*", + "test/**/*", + "tsconfig.json", + "rollup.config.mjs", + "../../tsconfig.json" + ], + "output": [ + "lib", + ".rollup.cache", + "./tsbuildinfo" + ], + "clean": "if-file-deleted" + }, + "build:types": { + "command": "tsc --build --pretty --noEmit" + }, + "test": { + "dependencies": [ + "test:all" + ] + }, + "test:all": { + "dependencies": [ + "build" + ], + "files": [ + "package.json" + ], + "output": [], + "command": "ava --color" + } + }, + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "typings": "lib/cjs/src/index.d.ts", + "files": [ + "lib" + ], + "exports": { + "types": "./lib/cjs/src/index.d.ts", + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.2", + "ava": "^6.2.0", + "rollup": "^4.39.0", + "ts-node": "^10.9.1", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "typescript": "^5" + }, + "ava": { + "cache": true, + "timeout": "120s", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register/transpile-only" + ], + "files": [ + "test/**/*.ts", + "!test/helpers/**/*" + ] + } +} diff --git a/packages/ts-simple-type/rollup.config.mjs b/packages/ts-simple-type/rollup.config.mjs new file mode 100644 index 00000000..0d1675f7 --- /dev/null +++ b/packages/ts-simple-type/rollup.config.mjs @@ -0,0 +1,51 @@ +import ts from "@rollup/plugin-typescript"; + +// create a require +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const { dirname } = require("path"); +const pkg = require("./package.json"); + +const input = "src/index.ts"; +const watch = { + include: "src/**" +}; + +export default [ + // Standard module config + { + input, + output: [ + { + dir: dirname(pkg.module), + format: "esm", + chunkFileNames: "chunk-[name]-[hash].js" + } + ], + plugins: [ + ts({ + tsconfig: "./tsconfig.prod.json", + outDir: "./lib/esm" + }) + ], + watch + }, + // CommonJS config + { + input, + output: [ + { + dir: dirname(pkg.main), + format: "cjs", + chunkFileNames: "chunk-[name]-[hash].js" + } + ], + plugins: [ + ts({ + tsconfig: "./tsconfig.prod.json", + outDir: "./lib/cjs" + }) + ], + watch + } +]; diff --git a/packages/ts-simple-type/run-playground.ts b/packages/ts-simple-type/run-playground.ts new file mode 100644 index 00000000..43ece0ab --- /dev/null +++ b/packages/ts-simple-type/run-playground.ts @@ -0,0 +1,208 @@ +/* eslint-disable no-console */ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { basename, relative, resolve } from "path"; +import type { CompilerOptions, Program, SourceFile, TypeChecker } from "typescript"; +import { convertCompilerOptionsFromJson, createProgram, findConfigFile, readConfigFile } from "typescript"; +import { isAssignableToSimpleType } from "./src/is-assignable/is-assignable-to-simple-type"; +import { deserializeSimpleType, serializeSimpleType } from "./src/transform/serialize-simple-type"; +import type { SimpleType } from "./src/simple-type"; +import { isSimpleType } from "./src/simple-type"; +import { toSimpleType } from "./src/transform/to-simple-type"; +import { visitNodeComparisons } from "./test/helpers/visit-type-comparisons"; + +const PLAYGROUND_DIRECTORY = resolve("playground"); +const PLAYGROUND_PATH_SF = resolve(PLAYGROUND_DIRECTORY, "playground.ts"); +const PLAYGROUND_PATH_CONFIG = resolve(PLAYGROUND_DIRECTORY, "tsconfig.json"); + +async function run() { + ensurePlayground(); + + const forceStrict = process.env.STRICT == null ? undefined : process.env.STRICT !== "false"; + const options = forceStrict != null ? { strict: forceStrict } : resolveTsConfigCompilerOptions(PLAYGROUND_DIRECTORY); + + const forceFile = process.env.FILE == null ? undefined : resolve(PLAYGROUND_DIRECTORY, process.env.FILE); + const program = createProgram([forceFile || PLAYGROUND_PATH_SF], options || {}); + + const sourceFile = Array.from(program.getSourceFiles()).find(f => f.fileName.includes(forceFile || "playground")); + if (sourceFile == null) { + throw new Error("Can't find playground.ts"); + } + + visitComparisons(sourceFile, program); +} + +function ensurePlayground(): void { + if (!existsSync(PLAYGROUND_DIRECTORY)) { + console.log(`Directory '${relative(process.cwd(), PLAYGROUND_DIRECTORY)}' doesn't exists. Creating directory...`); + mkdirSync(PLAYGROUND_DIRECTORY); + } + + if (!existsSync(PLAYGROUND_PATH_CONFIG)) { + console.log(`File '${relative(process.cwd(), PLAYGROUND_PATH_CONFIG)}' doesn't exists. Creating file...`); + const config = `{ + "files": [ + "playground.ts" + ], + "compilerOptions": { + "strict": true, + //"strictFunctionTypes": true, + //"strictNullChecks": true, + "target": "esnext", + "module": "commonjs", + "lib": [ + "esnext", + "dom" + ], + "outDir": "./dist" + } +}`; + + writeFileSync(PLAYGROUND_PATH_CONFIG, config); + } + + if (!existsSync(PLAYGROUND_PATH_SF)) { + console.log(`File ${relative(process.cwd(), PLAYGROUND_PATH_CONFIG)} doesn't exists. Creating example file...`); + const exampleCode = `interface A { + a: string; +} + +interface B { + b: string; +} + +// All assignments in this file are checked when running the playground. +// This file is in gitignore so no changes you make here are saved. +// Please use the following pattern when testing types in the playground. +{ const _: A = {} as B; } + + `; + + writeFileSync(PLAYGROUND_PATH_SF, exampleCode); + } +} + +function visitComparisons(sourceFile: SourceFile, program: Program) { + const options = program.getCompilerOptions(); + const checker = program.getTypeChecker(); + + const { strict, strictNullChecks, strictFunctionTypes } = options; + console.log(`Compiler options:`, { strict, strictNullChecks, strictFunctionTypes }); + + const onlyLine = process.env.LINE != null ? Number(process.env.LINE) : undefined; + if (onlyLine != null) { + console.log(`Only check line ${onlyLine}`); + } + + const debug = process.env.DEBUG != null && process.env.DEBUG !== "false"; + const eager = process.env.EAGER != null && process.env.EAGER !== "false"; + if (eager) { + console.log(`Eager types`); + } + + visitNodeComparisons(sourceFile, ({ line, nodeA, nodeB }) => { + line = line + 1; + + // Skip lines + if (onlyLine != null && line !== onlyLine) { + return; + } + + const typeA = checker.getTypeAtLocation(nodeA); + const typeB = checker.getTypeAtLocation(nodeB); + + const typeAString = checker.typeToString(typeA); + const typeBString = checker.typeToString(typeB); + + const expected = checker.isTypeAssignableTo(typeB, typeA); + + console.log(`\n------------- Checking line ${line} (${typeAString} === ${typeBString}) --------------`); + + // Get typeA + console.time("type-a-to-simple-type"); + const simpleTypeA = isSimpleType(typeA) ? typeA : toSimpleType(typeA, checker, { eager }); + console.timeEnd("type-a-to-simple-type"); + + // Get typeB + console.time("type-b-to-simple-type"); + const simpleTypeB = isSimpleType(typeB) ? typeB : toSimpleType(typeB, checker, { eager }); + console.timeEnd("type-b-to-simple-type"); + + // Run type checking + if (!debug) console.time("type-checking-lazy"); + const actual = (() => { + try { + return isAssignableToSimpleType(simpleTypeA, simpleTypeB, { ...options, debug }); + } catch (e) { + console.log(`isAssignableToSimpleType failed`); + console.error(e); + return null; + } + })(); + if (!debug) console.timeEnd("type-checking-lazy"); + + printType(simpleTypeA, checker, "TypeA"); + printType(simpleTypeB, checker, "TypeB"); + + // Log result + console.log(`\n##### Result #####`); + //console.log({ typeAString, typeBString }); + console.log({ actual, expected }); + + if (actual !== expected) { + throw new Error( + [ + expected + ? "Expected types to be assignable, but tsSimpleType returned 'false'" + : "Expected types not to be assignable, but tsSimpleType returned 'true'", + `Compiler options: ${JSON.stringify({ strict, strictNullChecks, strictFunctionTypes })}`, + `Line: ${line}`, + `Eager: ${eager}`, + `Assignment: (${typeAString} === ${typeBString})`, + onlyLine == null ? `> LINE=${line} DEBUG=true npm run playground` : "" + ].join("\n") + ); + } + }); +} + +function printType(simpleType: SimpleType, checker: TypeChecker, title: string) { + console.log(`\n//////////// Printing ${title} //////////////`); + + // Print the type + const serialized = serializeSimpleType(simpleType); + const typeCount = Object.keys(serialized.typeMap).length; + + if (typeCount > 50) { + console.log(`${title} is too big to console.log. It contains ${typeCount} unique types.`); + } else { + // Deserialize to unpack lazy types + const deserialized = deserializeSimpleType(serialized); + //console.log(`##### ${title} flat #####`); + //console.dir(serialized, { depth: 4 }); + //console.log(`\n##### ${title} nested #####`); + console.dir(deserialized, { depth: 6 }); + } +} + +run().catch(console.error); + +/** + * Resolves "tsconfig.json" file and returns its CompilerOptions + */ +export function resolveTsConfigCompilerOptions(directory: string = process.cwd()): CompilerOptions | undefined { + // Find the nearest tsconfig.json file if possible + const tsConfigFilePath = findConfigFile(directory, existsSync, "tsconfig.json"); + + if (tsConfigFilePath != null) { + // Read the tsconfig.json file + const parsedConfig = readConfigFile(tsConfigFilePath, path => readFileSync(path, "utf8")); + + if (parsedConfig != null && parsedConfig.config != null) { + // Parse the tsconfig.json file + const parsedJson = convertCompilerOptionsFromJson(parsedConfig.config.compilerOptions, basename(tsConfigFilePath), "tsconfig.json"); + return parsedJson?.options; + } + } + + return undefined; +} diff --git a/packages/ts-simple-type/src/constants.ts b/packages/ts-simple-type/src/constants.ts new file mode 100644 index 00000000..96a33fb5 --- /dev/null +++ b/packages/ts-simple-type/src/constants.ts @@ -0,0 +1,10 @@ +import type { Type } from "typescript"; +import type { SimpleType, SimpleTypeNever, SimpleTypeUnknown } from "./simple-type"; + +export const DEFAULT_TYPE_CACHE = new WeakMap(); + +export const DEFAULT_RESULT_CACHE = new Map>>(); + +export const DEFAULT_GENERIC_PARAMETER_TYPE: SimpleTypeUnknown = { kind: "UNKNOWN" }; + +export const NEVER_TYPE: SimpleTypeNever = { kind: "NEVER" }; diff --git a/packages/ts-simple-type/src/index.ts b/packages/ts-simple-type/src/index.ts new file mode 100644 index 00000000..7b446510 --- /dev/null +++ b/packages/ts-simple-type/src/index.ts @@ -0,0 +1,14 @@ +export * from "./simple-type"; +export * from "./ts-module"; + +export * from "./is-assignable/simple-type-comparison-options"; +export * from "./is-assignable/is-assignable-to-primitive-type"; +export * from "./is-assignable/is-assignable-to-type"; +export * from "./is-assignable/is-assignable-to-value"; +export * from "./is-assignable/is-assignable-to-simple-type-kind"; + +export * from "./transform/to-simple-type"; +export * from "./transform/type-to-string"; +export * from "./transform/serialize-simple-type"; + +export * from "./utils/validate-type"; diff --git a/packages/ts-simple-type/src/is-assignable/is-assignable-to-primitive-type.ts b/packages/ts-simple-type/src/is-assignable/is-assignable-to-primitive-type.ts new file mode 100644 index 00000000..bcc13778 --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/is-assignable-to-primitive-type.ts @@ -0,0 +1,17 @@ +import type { Type, TypeChecker } from "typescript"; +import type { SimpleType } from "../simple-type"; +import { PRIMITIVE_TYPE_KINDS } from "../simple-type"; +import { isTypeChecker } from "../utils/ts-util"; +import { isAssignableToSimpleTypeKind } from "./is-assignable-to-simple-type-kind"; + +/** + * Tests a type is assignable to a primitive type. + * @param type The type to test. + * @param options + */ +export function isAssignableToPrimitiveType(type: SimpleType): boolean; +export function isAssignableToPrimitiveType(type: Type | SimpleType, checker: TypeChecker): boolean; +export function isAssignableToPrimitiveType(type: Type | SimpleType, checkerOrOptions?: TypeChecker): boolean { + const checker = isTypeChecker(checkerOrOptions) ? checkerOrOptions : undefined; + return isAssignableToSimpleTypeKind(type, PRIMITIVE_TYPE_KINDS, checker!, { matchAny: true }); +} diff --git a/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type-kind.ts b/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type-kind.ts new file mode 100644 index 00000000..2b9c79ce --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type-kind.ts @@ -0,0 +1,70 @@ +import type { Type, TypeChecker } from "typescript"; +import type { SimpleType, SimpleTypeKind } from "../simple-type"; +import { isSimpleType } from "../simple-type"; +import { toSimpleType } from "../transform/to-simple-type"; +import { or } from "../utils/list-util"; +import { isTypeChecker } from "../utils/ts-util"; +import { validateType } from "../utils/validate-type"; +import type { SimpleTypeKindComparisonOptions } from "./simple-type-comparison-options"; + +/** + * Checks if a simple type kind is assignable to a type. + * @param type The type to check + * @param kind The simple type kind to check + * @param kind The simple type kind to check + * @param checker TypeCHecker if type is a typescript type + * @param options Options + */ +export function isAssignableToSimpleTypeKind( + type: SimpleType, + kind: SimpleTypeKind | SimpleTypeKind[], + options?: SimpleTypeKindComparisonOptions +): boolean; +export function isAssignableToSimpleTypeKind( + type: Type | SimpleType, + kind: SimpleTypeKind | SimpleTypeKind[], + checker: TypeChecker, + options?: SimpleTypeKindComparisonOptions +): boolean; +export function isAssignableToSimpleTypeKind( + type: Type | SimpleType, + kind: SimpleTypeKind | SimpleTypeKind[], + optionsOrChecker?: TypeChecker | SimpleTypeKindComparisonOptions, + options: SimpleTypeKindComparisonOptions = {} +): boolean { + const checker = isTypeChecker(optionsOrChecker) ? optionsOrChecker : undefined; + options = (isTypeChecker(optionsOrChecker) || optionsOrChecker == null ? options : optionsOrChecker) || {}; + + if (!isSimpleType(type)) { + return isAssignableToSimpleTypeKind(toSimpleType(type, checker!), kind, options); + } + + return validateType(type, simpleType => { + if (Array.isArray(kind) && or(kind, itemKind => simpleType.kind === itemKind)) { + return true; + } + + if (simpleType.kind === kind) { + return true; + } + + switch (simpleType.kind) { + // Make sure that an object without members are treated as ANY + case "OBJECT": { + if (simpleType.members == null || simpleType.members.length === 0) { + return isAssignableToSimpleTypeKind({ kind: "ANY" }, kind, options); + } + break; + } + + case "ANY": { + return options.matchAny || false; + } + + case "ENUM_MEMBER": { + return isAssignableToSimpleTypeKind(simpleType.type, kind, options); + } + } + return; + }); +} diff --git a/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type.ts b/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type.ts new file mode 100644 index 00000000..b4627ee3 --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/is-assignable-to-simple-type.ts @@ -0,0 +1,1128 @@ +import { DEFAULT_GENERIC_PARAMETER_TYPE, DEFAULT_RESULT_CACHE, NEVER_TYPE } from "../constants"; +import type { + SimpleType, + SimpleTypeFunctionParameter, + SimpleTypeGenericArguments, + SimpleTypeGenericParameter, + SimpleTypeIntersection, + SimpleTypeKind, + SimpleTypeMemberNamed, + SimpleTypeObject, + SimpleTypeObjectTypeBase, + SimpleTypeTuple +} from "../simple-type"; +import { isSimpleTypeLiteral, isSimpleTypePrimitive } from "../simple-type"; +import { simpleTypeToString } from "../transform/simple-type-to-string"; +import { and, or } from "../utils/list-util"; +import { resolveType as resolveTypeUnsafe } from "../utils/resolve-type"; +import { extendTypeParameterMap, getTupleLengthType } from "../utils/simple-type-util"; +import { isAssignableToSimpleTypeKind } from "./is-assignable-to-simple-type-kind"; +import type { SimpleTypeComparisonOptions } from "./simple-type-comparison-options"; + +interface IsAssignableToSimpleTypeInternalOptions { + config: SimpleTypeComparisonOptions; + cache: WeakMap>; + insideType: Set; + comparingTypes: Map>; + genericParameterMapA: Map; + genericParameterMapB: Map; + preventCaching: () => void; + operations: { value: number }; + depth: number; +} + +/** + * Returns if typeB is assignable to typeA. + * @param typeA Type A + * @param typeB Type B + * @param config + */ +export function isAssignableToSimpleType(typeA: SimpleType, typeB: SimpleType, config?: SimpleTypeComparisonOptions): boolean { + const userCache = config?.cache; + + config = { + ...config, + cache: undefined, + strict: config?.strict ?? true, + strictFunctionTypes: config?.strictFunctionTypes ?? config?.strict ?? true, + strictNullChecks: config?.strictNullChecks ?? config?.strict ?? true, + maxDepth: config?.maxDepth ?? 50, + maxOps: config?.maxOps ?? 1000 + }; + + const cacheKey = `${config.strict}:${config.strictFunctionTypes}:${config.strictNullChecks}`; + const cache = DEFAULT_RESULT_CACHE.get(cacheKey) || new WeakMap(); + DEFAULT_RESULT_CACHE.set(cacheKey, cache); + + return isAssignableToSimpleTypeCached(typeA, typeB, { + config, + operations: { value: 0 }, + depth: 0, + cache: userCache || cache, + insideType: new Set(), + comparingTypes: new Map(), + genericParameterMapA: new Map(), + genericParameterMapB: new Map(), + preventCaching: () => {} + }); +} + +function isAssignableToSimpleTypeCached(typeA: SimpleType, typeB: SimpleType, options: IsAssignableToSimpleTypeInternalOptions): boolean { + let typeACache = options.cache.get(typeA)!; + let preventCaching = false; + + if (typeACache?.has(typeB)) { + if (options.config.debug) { + logDebug( + options, + "caching", + `Found cache when comparing: ${simpleTypeToStringLazy(typeA)} (${typeA.kind}) and ${simpleTypeToStringLazy(typeB)} (${typeB.kind}). Cache content: ${typeACache.get(typeB)}` + ); + } + + return typeACache.get(typeB)!; + } + + // Call "isAssignableToSimpleTypeInternal" with a mutated options object + const result = isAssignableToSimpleTypeInternal(typeA, typeB, { + depth: options.depth, + operations: options.operations, + genericParameterMapA: options.genericParameterMapA, + genericParameterMapB: options.genericParameterMapB, + config: options.config, + insideType: options.insideType, + comparingTypes: options.comparingTypes, + cache: options.cache, + preventCaching: () => { + options.preventCaching(); + preventCaching = true; + } + }); + + if (!preventCaching) { + /*if (options.config.debug) { + logDebug( + options, + "caching", + `Setting cache for comparison between ${simpleTypeToStringLazy(typeA)} (${typeA.kind}) and ${simpleTypeToStringLazy(typeB)} (${typeB.kind}). Result: ${result}` + ); + }*/ + if (typeACache == null) { + typeACache = new WeakMap(); + options.cache.set(typeA, typeACache); + } + + typeACache.set(typeB, result); + } + + return result; +} + +function isCacheableType(simpleType: SimpleType, options: IsAssignableToSimpleTypeInternalOptions): boolean { + switch (simpleType.kind) { + case "UNION": + case "INTERSECTION": + if (options.genericParameterMapA.size !== 0 || options.genericParameterMapB.size !== 0) { + return false; + } + + break; + } + return !("typeParameters" in simpleType) && !["GENERIC_ARGUMENTS", "GENERIC_PARAMETER", "PROMISE", "LAZY"].includes(simpleType.kind); +} + +function isAssignableToSimpleTypeInternal(typeA: SimpleType, typeB: SimpleType, options: IsAssignableToSimpleTypeInternalOptions): boolean { + // It's assumed that the "options" parameter is already an unique reference that is safe to mutate. + + // Mutate depth and "operations" + options.depth = options.depth + 1; + options.operations.value++; + + // Handle debugging nested calls to isAssignable + if (options.config.debug === true) { + logDebugHeader(typeA, typeB, options); + } + + if (options.depth >= options.config.maxDepth! || options.operations.value >= options.config.maxOps!) { + options.preventCaching(); + return true; + } + + // When comparing types S and T, the relationship in question is assumed to be true + // for every directly or indirectly nested occurrence of the same S and the same T + if (options.comparingTypes.has(typeA)) { + if (options.comparingTypes.get(typeA)!.has(typeB)) { + options.preventCaching(); + + if (options.config.debug) { + logDebug(options, "comparing types", "Returns true because this relation is already being checking"); + } + + return true; + } + } + + // We might need a better way of handling refs, but these check are good for now + if (options.insideType.has(typeA) || options.insideType.has(typeB)) { + if (options.config.debug) { + logDebug( + options, + "inside type", + `{${typeA.kind}, ${typeB.kind}} {typeA: ${options.insideType.has(typeA)}} {typeB: ${options.insideType.has(typeB)}} {insideTypeMap: ${Array.from( + options.insideType.keys() + ) + .map(t => simpleTypeToStringLazy(t)) + .join()}}` + ); + } + + options.preventCaching(); + + return true; + } + + // Handle two types being equal + // Types are not necessarily equal if they have typeParams because we still need to check the actual generic arguments + if (isCacheableType(typeA, options) && isCacheableType(typeB, options)) { + if (typeA === typeB) { + if (options.config.debug) { + logDebug(options, "equal", "The two types are equal!", typeA.kind, typeB.kind); + } + return true; + } + } else { + options.preventCaching(); + } + + // Make it possible to overwrite default behavior by running user defined logic for comparing types + if (options.config.isAssignable != null) { + const result = options.config.isAssignable(typeA, typeB, options.config); + if (result != null) { + //options.preventCaching(); + return result; + } + } + + // Any and unknown. Everything is assignable to "ANY" and "UNKNOWN" + if (typeA.kind === "UNKNOWN" || typeA.kind === "ANY") { + return true; + } + + // Mutate options and add this comparison to "comparingTypes". + // Only do this if one of the types is not a primitive to save memory. + if (!isSimpleTypePrimitive(typeA) && !isSimpleTypePrimitive(typeB)) { + const comparingTypes = new Map(options.comparingTypes); + + if (comparingTypes.has(typeA)) { + comparingTypes.get(typeA)!.add(typeB); + } else { + comparingTypes.set(typeA, new Set([typeB])); + } + + options.comparingTypes = comparingTypes; + } + + // ##################### + // Expand typeB + // ##################### + switch (typeB.kind) { + // [typeB] (expand) + case "UNION": { + // Some types seems to absorb other types when type checking a union (eg. 'unknown'). + // Usually typescript will absorb those types for us, but not when working with generic parameters. + // The following line needs to be improved. + const types = typeB.types.filter(t => resolveType(t, options.genericParameterMapB) !== DEFAULT_GENERIC_PARAMETER_TYPE); + return and(types, childTypeB => isAssignableToSimpleTypeCached(typeA, childTypeB, options)); + } + + // [typeB] (expand) + case "INTERSECTION": { + // If we compare an intersection against an intersection, we need to compare from typeA and not typeB + // Example: [string, number] & [string] === [string, number] & [string] + if (typeA.kind === "INTERSECTION") { + break; + } + + const combined = reduceIntersectionIfPossible(typeB, options.genericParameterMapB); + + if (combined.kind === "NEVER") { + if (options.config.debug) { + logDebug(options, "intersection", `Combining types in intersection is impossible. Comparing with 'never' instead.`); + } + + return isAssignableToSimpleTypeCached(typeA, { kind: "NEVER" }, options); + } + + if (options.config.debug) { + if (combined !== typeB) { + logDebug(options, "intersection", `Types in intersection were combined into: ${simpleTypeToStringLazy(combined)}`); + } + } + + if (combined.kind !== "INTERSECTION") { + return isAssignableToSimpleTypeCached(typeA, combined, options); + } + + // An intersection type I is assignable to a type T if any type in I is assignable to T. + return or(combined.types, memberB => isAssignableToSimpleTypeCached(typeA, memberB, options)); + } + + // [typeB] (expand) + case "ALIAS": { + return isAssignableToSimpleTypeCached(typeA, typeB.target, options); + } + + // [typeB] (expand) + case "GENERIC_ARGUMENTS": { + const updatedGenericParameterMapB = extendTypeParameterMap(typeB, options.genericParameterMapB); + + if (options.config.debug) { + logDebug( + options, + "generic args", + "Expanding with typeB args: ", + Array.from(updatedGenericParameterMapB.entries()) + .map(([name, type]) => `${name}=${simpleTypeToStringLazy(type)}`) + .join("; "), + "typeParameters" in typeB.target ? "" : "[No type parameters in target!]" + ); + } + + return isAssignableToSimpleTypeCached(typeA, typeB.target, { + ...options, + genericParameterMapB: updatedGenericParameterMapB + }); + } + + // [typeB] (expand) + case "GENERIC_PARAMETER": { + const resolvedArgument = options.genericParameterMapB.get(typeB.name); + const realTypeB = resolvedArgument || typeB.default || DEFAULT_GENERIC_PARAMETER_TYPE; + + if (options.config.debug) { + logDebug( + options, + "generic", + `Resolving typeB for param ${typeB.name} to:`, + simpleTypeToStringLazy(realTypeB), + ", Default: ", + simpleTypeToStringLazy(typeB.default), + ", In map: ", + options.genericParameterMapB.has(typeB.name), + ", GenericParamMapB: ", + Array.from(options.genericParameterMapB.entries()) + .map(([name, t]) => `${name}=${simpleTypeToStringLazy(t)}`) + .join("; ") + ); + } + + return isAssignableToSimpleTypeCached(typeA, realTypeB, options); + } + } + + // ##################### + // Compare typeB + // ##################### + switch (typeB.kind) { + // [typeB] (compare) + case "ENUM_MEMBER": { + return isAssignableToSimpleTypeCached(typeA, typeB.type, options); + } + + // [typeB] (compare) + case "ENUM": { + return and(typeB.types, childTypeB => isAssignableToSimpleTypeCached(typeA, childTypeB, options)); + } + + // [typeB] (compare) + case "UNDEFINED": + case "NULL": { + // When strict null checks are turned off, "undefined" and "null" are in the domain of every type but never + if (!options.config.strictNullChecks) { + return typeA.kind !== "NEVER"; + } + + break; + } + + // [typeB] (compare) + case "ANY": { + // "any" can be assigned to anything but "never" + return typeA.kind !== "NEVER"; + } + + // [typeB] (compare) + case "NEVER": { + // "never" can be assigned to anything + return true; + } + } + + // ##################### + // Expand typeA + // ##################### + switch (typeA.kind) { + // [typeA] (expand) + case "ALIAS": { + return isAssignableToSimpleTypeCached(typeA.target, typeB, options); + } + + // [typeA] (expand) + case "GENERIC_PARAMETER": { + const resolvedArgument = options.genericParameterMapA.get(typeA.name); + const realTypeA = resolvedArgument || typeA.default || DEFAULT_GENERIC_PARAMETER_TYPE; + + if (options.config.debug) { + logDebug( + options, + "generic", + `Resolving typeA for param ${typeA.name} to:`, + simpleTypeToStringLazy(realTypeA), + ", Default: ", + simpleTypeToStringLazy(typeA.default), + ", In map: ", + options.genericParameterMapA.has(typeA.name), + ", GenericParamMapA: ", + Array.from(options.genericParameterMapA.entries()) + .map(([name, t]) => `${name}=${simpleTypeToStringLazy(t)}`) + .join("; ") + ); + } + + return isAssignableToSimpleTypeCached(realTypeA, typeB, options); + } + + // [typeA] (expand) + case "GENERIC_ARGUMENTS": { + const updatedGenericParameterMapA = extendTypeParameterMap(typeA, options.genericParameterMapA); + + if (options.config.debug) { + logDebug( + options, + "generic args", + "Expanding with typeA args: ", + Array.from(updatedGenericParameterMapA.entries()) + .map(([name, type]) => `${name}=${simpleTypeToStringLazy(type)}`) + .join("; "), + "typeParameters" in typeA.target ? "" : "[No type parameters in target!]" + ); + } + + return isAssignableToSimpleTypeCached(typeA.target, typeB, { + ...options, + genericParameterMapA: updatedGenericParameterMapA + }); + } + + // [typeA] (expand) + case "UNION": { + // Some types seems to absorb other types when type checking a union (eg. 'unknown'). + // Usually typescript will absorb those types for us, but not when working with generic parameters. + // The following line needs to be improved. + const types = typeA.types.filter( + t => resolveType(t, options.genericParameterMapA) !== DEFAULT_GENERIC_PARAMETER_TYPE || typeB === DEFAULT_GENERIC_PARAMETER_TYPE + ); + return or(types, childTypeA => isAssignableToSimpleTypeCached(childTypeA, typeB, options)); + } + + // [typeA] (expand) + case "INTERSECTION": { + const combined = reduceIntersectionIfPossible(typeA, options.genericParameterMapA); + + if (combined.kind === "NEVER") { + if (options.config.debug) { + logDebug(options, "intersection", `Combining types in intersection is impossible. Comparing with 'never' instead.`); + } + + return isAssignableToSimpleTypeCached({ kind: "NEVER" }, typeB, options); + } + + if (options.config.debug) { + if (combined !== typeA) { + logDebug(options, "intersection", `Types in intersection were combined into: ${simpleTypeToStringLazy(combined)}`); + } + } + + if (combined.kind !== "INTERSECTION") { + return isAssignableToSimpleTypeCached(combined, typeB, options); + } + + // A type T is assignable to an intersection type I if T is assignable to each type in I. + return and(combined.types, memberA => isAssignableToSimpleTypeCached(memberA, typeB, options)); + } + } + + // ##################### + // Compare typeA + // ##################### + switch (typeA.kind) { + // [typeA] (compare) + case "NON_PRIMITIVE": { + if (options.config.debug) { + logDebug(options, "object", `Checking if typeB is non-primitive [primitive=${isSimpleTypePrimitive(typeB)}] [hasName=${typeB.name != null}]`); + } + + if (isSimpleTypePrimitive(typeB)) { + return typeB.name != null; + } + + return typeB.kind !== "UNKNOWN"; + } + + // [typeA] (compare) + case "ARRAY": { + if (typeB.kind === "ARRAY") { + return isAssignableToSimpleTypeCached(typeA.type, typeB.type, options); + } else if (typeB.kind === "TUPLE") { + return and(typeB.members, memberB => isAssignableToSimpleTypeCached(typeA.type, memberB.type, options)); + } + + return false; + } + + // [typeA] (compare) + case "ENUM": { + return or(typeA.types, childTypeA => isAssignableToSimpleTypeCached(childTypeA, typeB, options)); + } + + // [typeA] (compare) + case "NUMBER_LITERAL": + case "STRING_LITERAL": + case "BIG_INT_LITERAL": + case "BOOLEAN_LITERAL": + case "ES_SYMBOL_UNIQUE": { + return isSimpleTypeLiteral(typeB) ? typeA.value === typeB.value : false; + } + + // [typeA] (compare) + case "ENUM_MEMBER": { + // You can always assign a "number" to a "number literal" enum member type. + if (resolveType(typeA.type, options.genericParameterMapA).kind === "NUMBER_LITERAL" && ["NUMBER"].includes(typeB.kind)) { + if (typeB.name != null) { + return false; + } + + return true; + } + + return isAssignableToSimpleTypeCached(typeA.type, typeB, options); + } + + // [typeA] (compare) + case "STRING": + case "BOOLEAN": + case "NUMBER": + case "ES_SYMBOL": + case "BIG_INT": { + if (typeB.name != null) { + return false; + } + + if (isSimpleTypeLiteral(typeB)) { + return PRIMITIVE_TYPE_TO_LITERAL_MAP[typeA.kind] === typeB.kind; + } + + return typeA.kind === typeB.kind; + } + + // [typeA] (compare) + case "UNDEFINED": + case "NULL": { + return typeA.kind === typeB.kind; + } + + // [typeA] (compare) + case "VOID": { + return typeB.kind === "VOID" || typeB.kind === "UNDEFINED"; + } + + // [typeA] (compare) + case "NEVER": { + return false; + } + + // [typeA] (compare) + // https://www.typescriptlang.org/docs/handbook/type-compatibility.html#comparing-two-functions + case "FUNCTION": + case "METHOD": { + if ("call" in typeB && typeB.call != null) { + return isAssignableToSimpleTypeCached(typeA, typeB.call, options); + } + + if (typeB.kind !== "FUNCTION" && typeB.kind !== "METHOD") return false; + + if (typeB.parameters == null || typeB.returnType == null) return typeA.parameters == null || typeA.returnType == null; + if (typeA.parameters == null || typeA.returnType == null) return true; + + // Any return type is assignable to void + if (options.config.debug) { + logDebug(options, "function", `Checking if return type of typeA is 'void'`); + } + + if (!isAssignableToSimpleTypeKind(typeA.returnType, "VOID")) { + //if (!isAssignableToSimpleTypeInternal(typeA.returnType, { kind: "VOID" }, options)) { + if (options.config.debug) { + logDebug(options, "function", `Return type is not void. Checking return types`); + } + + if (!isAssignableToSimpleTypeCached(typeA.returnType, typeB.returnType, options)) { + return false; + } + } + + // Test "this" types + const typeAThisParam = typeA.parameters.find(arg => arg.name === "this"); + const typeBThisParam = typeB.parameters.find(arg => arg.name === "this"); + + if (typeAThisParam != null && typeBThisParam != null) { + if (options.config.debug) { + logDebug(options, "function", `Checking 'this' param`); + } + + if (!isAssignableToSimpleTypeCached(typeAThisParam.type, typeBThisParam.type, options)) { + return false; + } + } + + // Get all "non-this" params + const paramTypesA = typeAThisParam == null ? typeA.parameters : typeA.parameters.filter(arg => arg !== typeAThisParam); + const paramTypesB = typeBThisParam == null ? typeB.parameters : typeB.parameters.filter(arg => arg !== typeBThisParam); + + // A function with 0 params can be assigned to any other function + if (paramTypesB.length === 0) { + return true; + } + + // A function with more required params than typeA isn't assignable + const requiredParamCountB = paramTypesB.reduce((sum, param) => (param.optional || param.rest ? sum : sum + 1), 0); + if (requiredParamCountB > paramTypesA.length) { + if (options.config.debug) { + logDebug(options, "function", `typeB has more required params than typeA: ${requiredParamCountB} > ${paramTypesA.length}`); + } + return false; + } + + let prevParamA: SimpleTypeFunctionParameter | undefined = undefined; + let prevParamB: SimpleTypeFunctionParameter | undefined = undefined; + + // Compare the types of each param + for (let i = 0; i < Math.max(paramTypesA.length, paramTypesB.length); i++) { + let paramA = paramTypesA[i]; + let paramB = paramTypesB[i]; + + if (options.config.debug) { + logDebug( + options, + "function", + `${i} ['${paramA?.name || "???"}' AND '${paramB?.name || "???"}'] Checking parameters ${options.config.strictFunctionTypes ? "[contravariant]" : "[bivariant]"}: [${ + paramA?.type == null ? "???" : simpleTypeToStringLazy(paramA.type) + } AND ${paramB?.type == null ? "???" : simpleTypeToStringLazy(paramB.type)}]` + ); + } + + // Try to find the last param in typeA. If it's a rest param, continue with that one + if (paramA == null && prevParamA?.rest) { + if (options.config.debug) { + logDebug(options, "function", `paramA is null and but last param in typeA is rest. Use that one.`); + } + + paramA = prevParamA; + } + + // Try to find the last param in typeB. If it's a rest param, continue with that one + if (paramB == null && prevParamB?.rest) { + if (options.config.debug) { + logDebug(options, "function", `paramB is null and but last param in typeB is rest. Use that one.`); + } + + paramB = prevParamB; + } + + prevParamA = paramA; + prevParamB = paramB; + + // If paramA is not present, check if paramB is optional or not present as well + if (paramA == null) { + if (paramB != null && !paramB.optional && !paramB.rest) { + if (options.config.debug) { + logDebug(options, "function", `paramA is null and paramB is null, optional or has rest`); + } + return false; + } + + if (options.config.debug) { + logDebug(options, "function", `paramA is null and paramB it not null, but is optional or has rest`); + } + + continue; + } + + // If paramB isn't present, check if paramA is optional + if (paramB == null) { + if (options.config.debug) { + logDebug(options, "function", `paramB is 'null' returning true`); + } + return true; + } + + // Check if we are comparing a spread against a non-spread + const resolvedTypeA = resolveType(paramA.type, options.genericParameterMapA); + const resolvedTypeB = resolveType(paramB.type, options.genericParameterMapB); + + // Unpack the array of rest parameters if possible + const paramAType = paramA.rest && resolvedTypeA.kind === "ARRAY" ? resolvedTypeA.type : paramA.type; + const paramBType = paramB.rest && resolvedTypeB.kind === "ARRAY" ? resolvedTypeB.type : paramB.type; + if (paramA.rest) { + if (options.config.debug) { + logDebug(options, "function", `paramA is 'rest' and has been resolved to '${simpleTypeToStringLazy(paramAType)}'`); + } + } + + if (paramB.rest) { + if (options.config.debug) { + logDebug(options, "function", `paramB is 'rest' and has been resolved to '${simpleTypeToStringLazy(paramBType)}'`); + } + } + + // Check if the param types are assignable + // Function parameter type checking is bivariant (when strictFunctionTypes is off) and contravariant (when strictFunctionTypes is on) + if (!options.config.strictFunctionTypes) { + if (options.config.debug) { + logDebug(options, "function", `Checking covariant relationship`); + } + + // Strict is off, therefore start by checking the covariant. + // The contravariant relationship will be checked afterwards resulting in bivariant behavior + if (isAssignableToSimpleTypeCached(paramAType, paramBType, options)) { + // Continue to next parameter + continue; + } + } + + // There is something strange going on where it seems checking two methods is less strict and checking two functions. + // I haven't found any documentation for this behavior, but it seems to be the case. + /* Examples (with strictFunctionTypes): + // ----------------------------- + interface I1 { + test(b: string | null): void; + } + interface I2 { + test(a: string): void; + } + + // This will not fail + const thisWillNotFail: I1 = {} as I2; + + // ----------------------------- + interface I3 { + test: (b: string | null) => void; + } + + interface I4 { + test: (a: string) => void; + } + + // This will fail with: + // Types of parameters 'a' and 'b' are incompatible. + // Type 'string | null' is not assignable to type 'string'. + // Type 'null' is not assignable to type 'string' + const thisWillFail: I3 = {} as I4; + */ + + const newOptions = { + ...options, + config: + typeA.kind === "METHOD" || typeB.kind === "METHOD" + ? { + ...options.config, + strictNullChecks: false, + strictFunctionTypes: false + } + : options.config, + cache: new WeakMap(), + genericParameterMapB: options.genericParameterMapA, + genericParameterMapA: options.genericParameterMapB + }; + + if (options.config.debug) { + logDebug(options, "function", `Checking contravariant relationship`); + } + + // Contravariant + if (!isAssignableToSimpleTypeCached(paramBType, paramAType, newOptions)) { + return false; + } + } + + return true; + } + + // [typeA] (compare) + case "INTERFACE": + case "OBJECT": + case "CLASS": { + // If there are no members check that "typeB" is not assignable to a set of incompatible type kinds + // This is to check the empty object {} and Object + const typeAHasZeroMembers = isObjectEmpty(typeA, { + ignoreOptionalMembers: (["UNKNOWN", "NON_PRIMITIVE"] as SimpleTypeKind[]).includes(typeB.kind) + }); + + if (typeAHasZeroMembers && typeA.call == null && (typeA.ctor == null || typeA.kind === "CLASS")) { + if (options.config.debug) { + logDebug(options, "object-type", `typeA is the empty object '{}'`); + } + + return !isAssignableToSimpleTypeKind( + typeB, + ["NULL", "UNDEFINED", "NEVER", "VOID", ...(options.config.strictNullChecks ? ["UNKNOWN"] : [])] as SimpleTypeKind[], + { + matchAny: false + } + ); + } + + switch (typeB.kind) { + case "FUNCTION": + case "METHOD": + return typeA.call != null && isAssignableToSimpleTypeCached(typeA.call, typeB, options); + + case "INTERFACE": + case "OBJECT": + case "CLASS": { + // Test both callable types + const membersA = typeA.members || []; + const membersB = typeB.members || []; + + options.insideType = new Set([...options.insideType, typeA, typeB]); + + // Check how many properties typeB has in common with typeA. + let membersInCommon = 0; + + // Make sure that every required prop in typeA is present in typeB + const requiredMembersInTypeAExistsInTypeB = and(membersA, memberA => { + //if (memberA.optional) return true; + const memberB = membersB.find(memberB => memberA.name === memberB.name); + if (memberB != null) membersInCommon += 1; + return memberB == null + ? // If corresponding "memberB" couldn't be found, return true if "memberA" is optional + memberA.optional + : // If corresponding "memberB" was found, return true if "memberA" is optional or "memberB" is not optional + memberA.optional || !memberB.optional; + }); + + if (!requiredMembersInTypeAExistsInTypeB) { + if (options.config.debug) { + logDebug(options, "object-type", `Didn't find required members from typeA in typeB`); + } + return false; + } + + // Check if construct signatures are assignable (if any) + if (typeA.ctor != null && typeA.kind !== "CLASS") { + if (options.config.debug) { + logDebug(options, "object-type", `Checking if typeB.ctor is assignable to typeA.ctor`); + } + + if (typeB.ctor != null && typeB.kind !== "CLASS") { + if (!isAssignableToSimpleTypeCached(typeA.ctor, typeB.ctor, options)) { + return false; + } + + membersInCommon += 1; + } else { + if (options.config.debug) { + logDebug(options, "object-type", `Expected typeB.ctor to have a ctor`); + } + return false; + } + } + + // Check if call signatures are assignable (if any) + if (typeA.call != null) { + if (options.config.debug) { + logDebug(options, "object-type", `Checking if typeB.call is assignable to typeA.call`); + } + + if (typeB.call != null) { + if (!isAssignableToSimpleTypeCached(typeA.call, typeB.call, options)) { + return false; + } + + membersInCommon += 1; + } else { + return false; + } + } + + // They are not assignable if typeB has 0 members in common with typeA, and there are more than 0 members in typeB. + // The ctor of classes are not counted towards if typeB is empty + const typeBIsEmpty = + membersB.length === 0 && typeB.call == null && ((typeB.kind !== "CLASS" && typeB.ctor == null) || typeB.kind === "CLASS"); + if (membersInCommon === 0 && !typeBIsEmpty) { + if (options.config.debug) { + logDebug(options, "object-type", `typeB has 0 members in common with typeA and there are more than 0 members in typeB`); + } + + return false; + } + + // Ensure that every member in typeB is assignable to corresponding members in typeA + const membersInTypeBAreAssignableToMembersInTypeA = and(membersB, memberB => { + const memberA = membersA.find(memberA => memberA.name === memberB.name); + if (memberA == null) { + return true; + } + if (options.config.debug) { + logDebug(options, "object-type", `Checking member '${memberA.name}' types`); + } + return isAssignableToSimpleTypeCached(memberA.type, memberB.type, options); + }); + + if (options.config.debug) { + if (!membersInTypeBAreAssignableToMembersInTypeA) { + logDebug(options, "object-type", `Not all members in typeB is assignable to corresponding members in typeA`); + } else { + logDebug(options, "object-type", `All members were checked successfully`); + } + } + + return membersInTypeBAreAssignableToMembersInTypeA; + } + default: + return false; + } + } + + // [typeA] (compare) + case "TUPLE": { + if (typeB.kind !== "TUPLE") return false; + + // Compare the length of each tuple, but compare the length type instead of the actual length + // We compare the length type because Typescript compares the type of the "length" member of tuples + if (!isAssignableToSimpleTypeCached(getTupleLengthType(typeA), getTupleLengthType(typeB), options)) { + return false; + } + + // Compare if typeB elements are assignable to typeA's rest element + // Example: [string, ...boolean[]] === [any, true, 123] + if (typeA.rest && typeB.members.length > typeA.members.length) { + return and(typeB.members.slice(typeA.members.length), (memberB, i) => { + return isAssignableToSimpleTypeCached(typeA.members[typeA.members.length - 1].type, memberB.type, options); + }); + } + + // Compare that every type of typeB is assignable to corresponding members in typeA + return and(typeA.members, (memberA, i) => { + const memberB = typeB.members[i]; + if (memberB == null) return memberA.optional; + return isAssignableToSimpleTypeCached(memberA.type, memberB.type, options); + }); + } + + // [typeA] (compare) + case "PROMISE": { + return typeB.kind === "PROMISE" && isAssignableToSimpleTypeCached(typeA.type, typeB.type, options); + } + + // [typeA] (compare) + case "DATE": { + return typeB.kind === "DATE"; + } + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - If we some how end up here (we shouldn't), return "true" as a safe fallback + return true; +} + +function reduceIntersectionIfPossible(simpleType: SimpleTypeIntersection, parameterMap: Map): SimpleType { + // DOCUMENTATION FROM TYPESCRIPT SOURCE CODE (getIntersectionType) + // We normalize combinations of intersection and union types based on the distributive property of the '&' + // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection + // types with union type constituents into equivalent union types with intersection type constituents and + // effectively ensure that union types are always at the top level in type representations. + // + // We do not perform structural deduplication on intersection types. Intersection types are created only by the & + // type operator and we can't reduce those because we want to support recursive intersection types. For example, + // a type alias of the form "type List = T & { next: List }" cannot be reduced during its declaration. + // Also, unlike union types, the order of the constituent types is preserved in order that overload resolution + // for intersections of types with signatures can be deterministic. + + // An intersection type is considered empty if it contains + // the type never, or + // more than one unit type or, + // an object type and a nullable type (null or undefined), or + // a string-like type and a type known to be non-string-like, or + // a number-like type and a type known to be non-number-like, or + // a symbol-like type and a type known to be non-symbol-like, or + // a void-like type and a type known to be non-void-like, or + // a non-primitive type and a type known to be primitive. + const typeKindMap = new Map(); + const primitiveSet = new Set(); + const primitiveLiteralSet = new Map(); + + for (const member of simpleType.types) { + const resolvedType = resolveType(member, parameterMap); + typeKindMap.set(resolvedType.kind, [...(typeKindMap.get(resolvedType.kind) || []), resolvedType]); + + switch (resolvedType.kind) { + case "NEVER": + return NEVER_TYPE; + } + + if (isSimpleTypePrimitive(resolvedType)) { + if (isSimpleTypeLiteral(resolvedType)) { + if (primitiveLiteralSet.has(resolvedType.kind) && primitiveLiteralSet.get(resolvedType.kind) !== resolvedType.value) { + return NEVER_TYPE; + } + + primitiveLiteralSet.set(resolvedType.kind, resolvedType.value); + } else { + primitiveSet.add(resolvedType.kind); + + if (primitiveSet.size > 1) { + return NEVER_TYPE; + } + } + } + } + + if ((typeKindMap.get("TUPLE")?.length || 0) > 1) { + let len: number | undefined = undefined; + for (const type of typeKindMap.get("TUPLE") as SimpleTypeTuple[]) { + if (len != null && len !== type.members.length) { + return NEVER_TYPE; + } + + len = type.members.length; + } + } + + if (typeKindMap.size === 1 && (typeKindMap.get("OBJECT")?.length || 0) > 1) { + const members = new Map(); + for (const type of typeKindMap.get("OBJECT") as SimpleTypeObject[]) { + for (const member of type.members || []) { + if (members.has(member.name)) { + const combinedMemberType = reduceIntersectionIfPossible( + { kind: "INTERSECTION", types: [members.get(member.name)!.type, member.type] }, + parameterMap + ); + if (combinedMemberType.kind === "NEVER") { + return combinedMemberType; + } + + members.set(member.name, { ...member, type: combinedMemberType }); + } else { + members.set(member.name, member); + } + } + } + + return { ...(typeKindMap.get("OBJECT")![0] as SimpleTypeObject), members: Array.from(members.values()) }; + } + + return simpleType; +} + +function isObjectEmpty(simpleType: SimpleTypeObjectTypeBase, { ignoreOptionalMembers }: { ignoreOptionalMembers?: boolean }): boolean { + return ( + simpleType.members == null || simpleType.members.length === 0 || (ignoreOptionalMembers && !simpleType.members.some(m => !m.optional)) || false + ); +} + +export function resolveType( + simpleType: SimpleType, + parameterMap: Map +): Exclude { + return resolveTypeUnsafe(simpleType, parameterMap); +} + +function logDebugHeader(typeA: SimpleType, typeB: SimpleType, options: IsAssignableToSimpleTypeInternalOptions): void { + const silentConfig = { ...options.config, debug: false, maxOps: 20, maxDepth: 20 }; + let result: boolean | string; + try { + result = isAssignableToSimpleType(typeA, typeB, silentConfig); + } catch (e) { + result = (e as Error).message; + } + const depthChars = " ".repeat(options.depth); + + const firstLogPart = ` ${depthChars}${simpleTypeToStringLazy(typeA)} ${colorText(options, ">:", "cyan")} ${simpleTypeToStringLazy(typeB)} [${typeA.kind} === ${typeB.kind}]`; + let text = `${firstLogPart} ${" ".repeat(Math.max(2, 120 - firstLogPart.length))}${colorText(options, options.depth, "yellow")} ### (${typeA.name || "???"} === ${ + typeB.name || "???" + }) [result=${colorText(options, result, result === true ? "green" : "red")}]`; + + if (options.depth >= 50) { + // Too deep + if (options.depth === 50) { + text = `Nested comparisons reach 100. Skipping logging...`; + } else { + return; + } + } + + // eslint-disable-next-line no-console + (options.config.debugLog || console.log)(text); +} + +function logDebug(options: IsAssignableToSimpleTypeInternalOptions, title: string, ...args: unknown[]): void { + const depthChars = " ".repeat(options.depth); + + const text = `${depthChars} [${colorText(options, title, "blue")}] ${args.join(" ")}`; + + // eslint-disable-next-line no-console + (options.config.debugLog || console.log)(colorText(options, text, "gray")); +} + +function simpleTypeToStringLazy(simpleType: SimpleType | undefined): string { + if (simpleType == null) { + return "???"; + } + return simpleTypeToString(simpleType); +} + +function colorText( + options: IsAssignableToSimpleTypeInternalOptions, + text: unknown, + color: "cyan" | "gray" | "red" | "blue" | "green" | "yellow" +): string { + if (options.config.debugLog != null) { + return `${text}`; + } + + const RESET = "\x1b[0m"; + const COLOR = (() => { + switch (color) { + case "gray": + return "\x1b[2m\x1b[37m"; + case "red": + return "\x1b[31m"; + case "green": + return "\x1b[32m"; + case "yellow": + return "\x1b[33m"; + case "blue": + return "\x1b[34m"; + case "cyan": + return "\x1b[2m\x1b[36m"; + } + })(); + + return `${COLOR}${text}${RESET}`; +} + +const PRIMITIVE_TYPE_TO_LITERAL_MAP = { + ["STRING"]: "STRING_LITERAL", + ["NUMBER"]: "NUMBER_LITERAL", + ["BOOLEAN"]: "BOOLEAN_LITERAL", + ["BIG_INT"]: "BIG_INT_LITERAL", + ["ES_SYMBOL"]: "ES_SYMBOL_UNIQUE" +} as unknown as Record; + +/*const LITERAL_TYPE_TO_PRIMITIVE_TYPE_MAP = ({ + ["STRING_LITERAL"]: "STRING", + ["NUMBER_LITERAL"]: "NUMBER", + ["BOOLEAN_LITERAL"]: "BOOLEAN", + ["BIG_INT_LITERAL"]: "BIG_INT", + ["ES_SYMBOL_UNIQUE"]: "ES_SYMBOL" +} as unknown) as Record;*/ diff --git a/packages/ts-simple-type/src/is-assignable/is-assignable-to-type.ts b/packages/ts-simple-type/src/is-assignable/is-assignable-to-type.ts new file mode 100644 index 00000000..8924a4e9 --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/is-assignable-to-type.ts @@ -0,0 +1,72 @@ +import type { Node, Program, Type, TypeChecker } from "typescript"; +import type { SimpleType } from "../simple-type"; +import { isSimpleType } from "../simple-type"; +import { toSimpleType } from "../transform/to-simple-type"; +import { isNode, isProgram, isTypeChecker } from "../utils/ts-util"; +import { isAssignableToSimpleType } from "./is-assignable-to-simple-type"; +import type { SimpleTypeComparisonOptions } from "./simple-type-comparison-options"; + +/** + * Tests if "typeA = typeB" in strict mode. + * @param typeA - Type A + * @param typeB - Type B + * @param checkerOrOptions + * @param options + */ +export function isAssignableToType(typeA: SimpleType, typeB: SimpleType, options?: SimpleTypeComparisonOptions): boolean; +export function isAssignableToType( + typeA: SimpleType | Type | Node, + typeB: SimpleType | Type | Node, + checker: TypeChecker | Program, + options?: SimpleTypeComparisonOptions +): boolean; +export function isAssignableToType( + typeA: Type | Node, + typeB: Type | Node, + checker: TypeChecker | Program, + options?: SimpleTypeComparisonOptions +): boolean; +export function isAssignableToType( + typeA: Type | Node | SimpleType, + typeB: Type | Node | SimpleType, + checker: Program | TypeChecker, + options?: SimpleTypeComparisonOptions +): boolean; +export function isAssignableToType( + typeA: Type | Node | SimpleType, + typeB: Type | Node | SimpleType, + checkerOrOptions?: TypeChecker | Program | SimpleTypeComparisonOptions, + options?: SimpleTypeComparisonOptions +): boolean { + if (typeA === typeB) return true; + + // Get the correct TypeChecker + const checker = isTypeChecker(checkerOrOptions) ? checkerOrOptions : isProgram(checkerOrOptions) ? checkerOrOptions.getTypeChecker() : undefined; + + // Get the correct options. Potentially merge user given options with program options. + options = { + ...(checkerOrOptions == null + ? {} + : isProgram(checkerOrOptions) + ? checkerOrOptions.getCompilerOptions() + : isTypeChecker(checkerOrOptions) + ? {} + : checkerOrOptions), + ...(options || {}) + }; + + // Check if the types are nodes (in which case we need to get the type of the node) + typeA = isNode(typeA) ? checker!.getTypeAtLocation(typeA) : typeA; + typeB = isNode(typeB) ? checker!.getTypeAtLocation(typeB) : typeB; + + // Use native "isTypeAssignableTo" if both types are native TS-types and "isTypeAssignableTo" is exposed on TypeChecker + if (!isSimpleType(typeA) && !isSimpleType(typeB) && checker != null && checker.isTypeAssignableTo != null) { + return checker.isTypeAssignableTo(typeB, typeA); + } + + // Convert the TS types to SimpleTypes + const simpleTypeA = isSimpleType(typeA) ? typeA : toSimpleType(typeA, checker!); + const simpleTypeB = isSimpleType(typeB) ? typeB : toSimpleType(typeB, checker!); + + return isAssignableToSimpleType(simpleTypeA, simpleTypeB, options); +} diff --git a/packages/ts-simple-type/src/is-assignable/is-assignable-to-value.ts b/packages/ts-simple-type/src/is-assignable/is-assignable-to-value.ts new file mode 100644 index 00000000..961c3630 --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/is-assignable-to-value.ts @@ -0,0 +1,105 @@ +import type { Node, Program, Type, TypeChecker } from "typescript"; +import type { SimpleType, SimpleTypeMemberNamed } from "../simple-type"; +import { isAssignableToType } from "./is-assignable-to-type"; + +/** + * Tests if a type is assignable to a value. + * Tests "type = value" in strict mode. + * @param type The type to test. + * @param value The value to test. + */ +export function isAssignableToValue(type: SimpleType, value: unknown): boolean; +export function isAssignableToValue(type: SimpleType | Type | Node, value: unknown, checker: TypeChecker | Program): boolean; +export function isAssignableToValue(type: SimpleType | Type | Node, value: unknown, checker?: TypeChecker | Program): boolean { + const typeB = convertValueToSimpleType(value, { visitValueSet: new Set(), widening: false }); + return isAssignableToType(type, typeB, checker!, { strict: true }); +} + +function convertValueToSimpleType(value: unknown, { visitValueSet, widening }: { visitValueSet: Set; widening: boolean }): SimpleType { + if (visitValueSet.has(value)) { + return { kind: "ANY" }; + } + + if (value === undefined) { + return { + kind: "UNDEFINED" + }; + } else if (value === null) { + return { + kind: "NULL" + }; + } else if (typeof value === "string") { + if (widening) { + return { kind: "STRING" }; + } + + return { + kind: "STRING_LITERAL", + value + }; + } else if (typeof value === "number") { + if (widening) { + return { kind: "NUMBER" }; + } + + return { + kind: "NUMBER_LITERAL", + value + }; + } else if (typeof value === "boolean") { + if (widening) { + return { kind: "BOOLEAN" }; + } + + return { + kind: "BOOLEAN_LITERAL", + value + }; + } else if (typeof value === "symbol") { + if (widening) { + return { kind: "ES_SYMBOL" }; + } + + return { + kind: "ES_SYMBOL_UNIQUE", + value: Math.random().toString() + }; + } else if (Array.isArray(value)) { + visitValueSet.add(value); + + const firstElement = value[0]; + if (firstElement != null) { + return { kind: "ARRAY", type: convertValueToSimpleType(firstElement, { visitValueSet, widening: true }) }; + } + return { + kind: "ARRAY", + type: { kind: "ANY" } + }; + } else if (value instanceof Promise) { + return { + kind: "PROMISE", + type: { kind: "ANY" } + }; + } else if (value instanceof Date) { + return { + kind: "DATE" + }; + } else if (typeof value === "object" && value != null) { + visitValueSet.add(value); + + const members = Object.entries(value).map( + ([key, value]) => + ({ + name: key, + type: convertValueToSimpleType(value, { visitValueSet, widening }) + }) as SimpleTypeMemberNamed + ); + + return { + kind: "OBJECT", + members + }; + } + + return { kind: "ANY" }; +} diff --git a/packages/ts-simple-type/src/is-assignable/simple-type-comparison-options.ts b/packages/ts-simple-type/src/is-assignable/simple-type-comparison-options.ts new file mode 100644 index 00000000..9273ae94 --- /dev/null +++ b/packages/ts-simple-type/src/is-assignable/simple-type-comparison-options.ts @@ -0,0 +1,21 @@ +import type { SimpleType } from "../simple-type"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface SimpleTypeBaseOptions {} + +export interface SimpleTypeComparisonOptions extends SimpleTypeBaseOptions { + strict?: boolean; + strictNullChecks?: boolean; + strictFunctionTypes?: boolean; + noStrictGenericChecks?: boolean; + isAssignable?: (typeA: SimpleType, typeB: SimpleType, options: SimpleTypeComparisonOptions) => boolean | undefined | void; + debug?: boolean; + debugLog?: (text: string) => void; + cache?: WeakMap>; + maxDepth?: number; + maxOps?: number; +} + +export interface SimpleTypeKindComparisonOptions extends SimpleTypeBaseOptions { + matchAny?: boolean; +} diff --git a/packages/ts-simple-type/src/simple-type.ts b/packages/ts-simple-type/src/simple-type.ts new file mode 100644 index 00000000..b0803cc4 --- /dev/null +++ b/packages/ts-simple-type/src/simple-type.ts @@ -0,0 +1,434 @@ +export type SimpleTypeKind = + // Primitives types + | "STRING_LITERAL" + | "NUMBER_LITERAL" + | "BOOLEAN_LITERAL" + | "BIG_INT_LITERAL" + | "ES_SYMBOL_UNIQUE" + | "STRING" + | "NUMBER" + | "BOOLEAN" + | "BIG_INT" + | "ES_SYMBOL" + | "NULL" + | "UNDEFINED" + | "VOID" + // TS-specific types + | "NEVER" + | "ANY" + | "UNKNOWN" + | "ENUM" + | "ENUM_MEMBER" + | "NON_PRIMITIVE" + // Structured types + | "UNION" + | "INTERSECTION" + // Object types types + | "INTERFACE" + | "OBJECT" + | "CLASS" + // Callable + | "FUNCTION" + | "METHOD" + // Generics + | "GENERIC_ARGUMENTS" + | "GENERIC_PARAMETER" + | "ALIAS" + // Lists + | "TUPLE" + | "ARRAY" + // Special types + | "DATE" + | "PROMISE"; + +export type SimpleTypeModifierKind = + | "EXPORT" + | "AMBIENT" + | "PUBLIC" + | "PRIVATE" + | "PROTECTED" + | "STATIC" + | "READONLY" + | "ABSTRACT" + | "ASYNC" + | "DEFAULT"; + +// ############################## +// Base +// ############################## +export interface SimpleTypeBase { + readonly kind: SimpleTypeKind; + readonly name?: string; +} + +// ############################## +// Primitive Types +// ############################## +export interface SimpleTypeBigIntLiteral extends SimpleTypeBase { + readonly kind: "BIG_INT_LITERAL"; + readonly value: bigint; +} + +export interface SimpleTypeStringLiteral extends SimpleTypeBase { + readonly kind: "STRING_LITERAL"; + readonly value: string; +} + +export interface SimpleTypeNumberLiteral extends SimpleTypeBase { + readonly kind: "NUMBER_LITERAL"; + readonly value: number; +} + +export interface SimpleTypeBooleanLiteral extends SimpleTypeBase { + readonly kind: "BOOLEAN_LITERAL"; + readonly value: boolean; +} + +export interface SimpleTypeString extends SimpleTypeBase { + readonly kind: "STRING"; +} + +export interface SimpleTypeNumber extends SimpleTypeBase { + readonly kind: "NUMBER"; +} + +export interface SimpleTypeBoolean extends SimpleTypeBase { + readonly kind: "BOOLEAN"; +} + +export interface SimpleTypeBigInt extends SimpleTypeBase { + readonly kind: "BIG_INT"; +} + +export interface SimpleTypeESSymbol extends SimpleTypeBase { + readonly kind: "ES_SYMBOL"; +} + +export interface SimpleTypeESSymbolUnique extends SimpleTypeBase { + readonly kind: "ES_SYMBOL_UNIQUE"; + readonly value: string; +} + +// ############################## +// TS-specific types +// ############################## + +export interface SimpleTypeNull extends SimpleTypeBase { + readonly kind: "NULL"; +} + +export interface SimpleTypeNever extends SimpleTypeBase { + readonly kind: "NEVER"; +} + +export interface SimpleTypeUndefined extends SimpleTypeBase { + readonly kind: "UNDEFINED"; +} + +export interface SimpleTypeAny extends SimpleTypeBase { + readonly kind: "ANY"; +} + +export interface SimpleTypeUnknown extends SimpleTypeBase { + readonly kind: "UNKNOWN"; +} + +export interface SimpleTypeVoid extends SimpleTypeBase { + readonly kind: "VOID"; +} + +export interface SimpleTypeNonPrimitive extends SimpleTypeBase { + readonly kind: "NON_PRIMITIVE"; +} + +export interface SimpleTypeEnumMember extends SimpleTypeBase { + readonly kind: "ENUM_MEMBER"; + readonly fullName: string; + readonly name: string; + readonly type: SimpleTypePrimitive; +} + +export interface SimpleTypeEnum extends SimpleTypeBase { + readonly name: string; + readonly kind: "ENUM"; + readonly types: SimpleTypeEnumMember[]; +} + +// ############################## +// Structure Types +// ############################## +export interface SimpleTypeUnion extends SimpleTypeBase { + readonly kind: "UNION"; + readonly types: SimpleType[]; +} + +export interface SimpleTypeIntersection extends SimpleTypeBase { + readonly kind: "INTERSECTION"; + readonly types: SimpleType[]; +} + +// ############################## +// Object Types +// ############################## + +export interface SimpleTypeMember { + readonly optional: boolean; + readonly type: SimpleType; + readonly modifiers?: SimpleTypeModifierKind[]; +} + +export interface SimpleTypeMemberNamed extends SimpleTypeMember { + readonly name: string; +} + +export interface SimpleTypeObjectTypeBase extends SimpleTypeBase { + readonly members?: SimpleTypeMemberNamed[]; + readonly ctor?: SimpleTypeFunction; + readonly call?: SimpleTypeFunction; + readonly typeParameters?: SimpleTypeGenericParameter[]; + readonly indexType?: { + ["STRING"]?: SimpleType; + ["NUMBER"]?: SimpleType; + }; +} + +export interface SimpleTypeInterface extends SimpleTypeObjectTypeBase { + readonly kind: "INTERFACE"; +} + +export interface SimpleTypeClass extends SimpleTypeObjectTypeBase { + readonly kind: "CLASS"; +} + +export interface SimpleTypeObject extends SimpleTypeObjectTypeBase { + readonly kind: "OBJECT"; +} + +// ############################## +// Callable +// ############################## + +export interface SimpleTypeFunctionParameter { + readonly name: string; + readonly type: SimpleType; + readonly optional: boolean; + readonly rest: boolean; + readonly initializer: boolean; +} + +export interface SimpleTypeFunction extends SimpleTypeBase { + readonly kind: "FUNCTION"; + readonly parameters?: SimpleTypeFunctionParameter[]; + readonly typeParameters?: SimpleTypeGenericParameter[]; + readonly returnType?: SimpleType; +} + +export interface SimpleTypeMethod extends SimpleTypeBase { + readonly kind: "METHOD"; + readonly parameters: SimpleTypeFunctionParameter[]; + readonly typeParameters?: SimpleTypeGenericParameter[]; + readonly returnType: SimpleType; +} + +// ############################## +// Generics +// ############################## + +export interface SimpleTypeGenericArguments extends SimpleTypeBase { + readonly kind: "GENERIC_ARGUMENTS"; + readonly name?: undefined; + readonly target: SimpleType; + readonly typeArguments: SimpleType[]; +} + +export interface SimpleTypeGenericParameter extends SimpleTypeBase { + readonly name: string; + readonly kind: "GENERIC_PARAMETER"; + readonly default?: SimpleType; +} + +export interface SimpleTypeAlias extends SimpleTypeBase { + readonly kind: "ALIAS"; + readonly name: string; + readonly target: SimpleType; + readonly typeParameters?: SimpleTypeGenericParameter[]; +} + +// ############################## +// Lists +// ############################## + +export interface SimpleTypeTuple extends SimpleTypeBase { + readonly kind: "TUPLE"; + readonly members: SimpleTypeMember[]; + readonly rest?: boolean; +} + +export interface SimpleTypeArray extends SimpleTypeBase { + readonly kind: "ARRAY"; + readonly type: SimpleType; +} + +// ############################## +// Special Types +// ############################## + +export interface SimpleTypeDate extends SimpleTypeBase { + readonly kind: "DATE"; +} + +export interface SimpleTypePromise extends SimpleTypeBase { + readonly kind: "PROMISE"; + readonly type: SimpleType; +} + +export type SimpleType = + | SimpleTypeBigIntLiteral + | SimpleTypeEnumMember + | SimpleTypeEnum + | SimpleTypeClass + | SimpleTypeFunction + | SimpleTypeObject + | SimpleTypeInterface + | SimpleTypeTuple + | SimpleTypeArray + | SimpleTypeUnion + | SimpleTypeIntersection + | SimpleTypeStringLiteral + | SimpleTypeNumberLiteral + | SimpleTypeBooleanLiteral + | SimpleTypeESSymbolUnique + | SimpleTypeString + | SimpleTypeNumber + | SimpleTypeBoolean + | SimpleTypeBigInt + | SimpleTypeESSymbol + | SimpleTypeNull + | SimpleTypeUndefined + | SimpleTypeNever + | SimpleTypeAny + | SimpleTypeMethod + | SimpleTypeVoid + | SimpleTypeNonPrimitive + | SimpleTypePromise + | SimpleTypeUnknown + | SimpleTypeAlias + | SimpleTypeDate + | SimpleTypeGenericArguments + | SimpleTypeGenericParameter; + +// Collect all values on place. This is a map so Typescript will complain if we forget any kind. +const SIMPLE_TYPE_MAP: Record = { + NUMBER_LITERAL: "primitive_literal", + STRING_LITERAL: "primitive_literal", + BIG_INT_LITERAL: "primitive_literal", + BOOLEAN_LITERAL: "primitive_literal", + ES_SYMBOL_UNIQUE: "primitive_literal", + BIG_INT: "primitive", + BOOLEAN: "primitive", + NULL: "primitive", + UNDEFINED: "primitive", + VOID: "primitive", + ES_SYMBOL: "primitive", + NUMBER: "primitive", + STRING: "primitive", + NON_PRIMITIVE: undefined, + ENUM_MEMBER: undefined, + ALIAS: undefined, + ANY: undefined, + ARRAY: undefined, + CLASS: undefined, + DATE: undefined, + ENUM: undefined, + FUNCTION: undefined, + GENERIC_ARGUMENTS: undefined, + GENERIC_PARAMETER: undefined, + INTERFACE: undefined, + INTERSECTION: undefined, + METHOD: undefined, + NEVER: undefined, + OBJECT: undefined, + PROMISE: undefined, + TUPLE: undefined, + UNION: undefined, + UNKNOWN: undefined +}; + +// Primitive, literal +export type SimpleTypeLiteral = + | SimpleTypeBigIntLiteral + | SimpleTypeBooleanLiteral + | SimpleTypeStringLiteral + | SimpleTypeNumberLiteral + | SimpleTypeESSymbolUnique; +export const LITERAL_TYPE_KINDS: SimpleTypeKind[] = (Object.keys(SIMPLE_TYPE_MAP) as SimpleTypeKind[]).filter( + kind => SIMPLE_TYPE_MAP[kind] === "primitive_literal" +); +export function isSimpleTypeLiteral(type: SimpleType): type is SimpleTypeLiteral { + return LITERAL_TYPE_KINDS.includes(type.kind); +} + +// Primitive +export type SimpleTypePrimitive = + | SimpleTypeLiteral + | SimpleTypeString + | SimpleTypeNumber + | SimpleTypeBoolean + | SimpleTypeBigInt + | SimpleTypeNull + | SimpleTypeUndefined + | SimpleTypeESSymbol; +export const PRIMITIVE_TYPE_KINDS: SimpleTypeKind[] = [ + ...LITERAL_TYPE_KINDS, + ...(Object.keys(SIMPLE_TYPE_MAP) as SimpleTypeKind[]).filter(kind => SIMPLE_TYPE_MAP[kind] === "primitive") +]; +export function isSimpleTypePrimitive(type: SimpleType): type is SimpleTypePrimitive { + return PRIMITIVE_TYPE_KINDS.includes(type.kind); +} + +// All kinds +export const SIMPLE_TYPE_KINDS = Object.keys(SIMPLE_TYPE_MAP) as SimpleTypeKind[]; +export function isSimpleType(type: unknown): type is SimpleType { + return ( + typeof type === "object" && + type != null && + "kind" in type && + Object.values(SIMPLE_TYPE_KINDS).find((key: SimpleTypeKind) => key === (type as { kind: SimpleTypeKind }).kind) != null + ); +} + +export type SimpleTypeKindMap = { + STRING_LITERAL: SimpleTypeStringLiteral; + NUMBER_LITERAL: SimpleTypeNumberLiteral; + BOOLEAN_LITERAL: SimpleTypeBooleanLiteral; + BIG_INT_LITERAL: SimpleTypeBigIntLiteral; + ES_SYMBOL_UNIQUE: SimpleTypeESSymbolUnique; + STRING: SimpleTypeString; + NUMBER: SimpleTypeNumber; + BOOLEAN: SimpleTypeBoolean; + BIG_INT: SimpleTypeBigInt; + ES_SYMBOL: SimpleTypeESSymbol; + NULL: SimpleTypeNull; + UNDEFINED: SimpleTypeUndefined; + VOID: SimpleTypeVoid; + NEVER: SimpleTypeNever; + ANY: SimpleTypeAny; + UNKNOWN: SimpleTypeUnknown; + ENUM: SimpleTypeEnum; + ENUM_MEMBER: SimpleTypeEnumMember; + NON_PRIMITIVE: SimpleTypeNonPrimitive; + UNION: SimpleTypeUnion; + INTERSECTION: SimpleTypeIntersection; + INTERFACE: SimpleTypeInterface; + OBJECT: SimpleTypeObject; + CLASS: SimpleTypeClass; + FUNCTION: SimpleTypeFunction; + METHOD: SimpleTypeMethod; + GENERIC_ARGUMENTS: SimpleTypeGenericArguments; + GENERIC_PARAMETER: SimpleTypeGenericParameter; + ALIAS: SimpleTypeAlias; + TUPLE: SimpleTypeTuple; + ARRAY: SimpleTypeArray; + DATE: SimpleTypeDate; + PROMISE: SimpleTypePromise; +}; diff --git a/packages/ts-simple-type/src/transform/serialize-simple-type.ts b/packages/ts-simple-type/src/transform/serialize-simple-type.ts new file mode 100644 index 00000000..9269ab0b --- /dev/null +++ b/packages/ts-simple-type/src/transform/serialize-simple-type.ts @@ -0,0 +1,156 @@ +import type { SimpleType } from "../simple-type"; +import { isSimpleType } from "../simple-type"; + +const TYPE_REF_PREFIX = "__REF__"; + +function isTypeRef(value: unknown): value is string { + return typeof value === "string" && value.startsWith(TYPE_REF_PREFIX); +} + +export type SerializedSimpleTypeWithRef = { + [key in keyof ST]: ST[key] extends SimpleType ? string : SerializedSimpleTypeWithRef; +}; + +export interface SerializedSimpleType { + typeMap: Record; + type: number; +} + +/** + * Deserialize a serialized type into a SimpleType + * @param serializedSimpleType + */ +export function deserializeSimpleType(serializedSimpleType: SerializedSimpleType): SimpleType { + const { typeMap } = serializedSimpleType; + + // Make a map to lookup ids to get a shared SimpleType + const deserializedTypeMap = new Map(); + + // Add an empty object for each type in the reference map. + // These object will be filled out afterwards. + // This is useful because it allows us to easily shared references. + for (const typeId of Object.keys(typeMap)) { + deserializedTypeMap.set(Number(typeId), {} as never); + } + + // Loop through all types and deserialize them + for (const [typeId, serializedType] of Object.entries(typeMap)) { + const deserializedType = convertObject(serializedType, obj => { + // Find and replace with a corresponding type in the typeMap when encountering a typeRef + if (isTypeRef(obj)) { + const typeId = Number(obj.replace(TYPE_REF_PREFIX, "")); + return deserializedTypeMap.get(typeId)!; + } + return; + }); + + // Merge the content of "deserialized type" into the reference + Object.assign(deserializedTypeMap.get(Number(typeId))!, deserializedType); + } + + // Return the main deserialized type + return deserializedTypeMap.get(serializedSimpleType.type)!; +} + +/** + * Serialize a SimpleType + * @param simpleType + */ +export function serializeSimpleType(simpleType: SimpleType): SerializedSimpleType { + // Assign an "id" to each serialized type + const typeMap: Record = {}; + // Make it possible to lookup an id based on a SimpleType + const typeMapReverse = new WeakMap(); + // Keep track of current id + let id = 0; + + const mainTypeId = serializeTypeInternal(simpleType, { + assignIdToType: type => { + if (typeMapReverse.has(type)) { + return typeMapReverse.get(type)!; + } + + const assignedId = id++; + typeMapReverse.set(type, assignedId); + return assignedId; + }, + getIdFromType: type => { + return typeMapReverse.get(type); + }, + emitType: (id, simpleTypeWithRef) => { + typeMap[id] = simpleTypeWithRef; + return id++; + } + }); + + return { + type: mainTypeId, + typeMap + }; +} + +function serializeTypeInternal( + simpleType: SimpleType, + { + emitType, + getIdFromType, + assignIdToType + }: { + emitType: (id: number, simpleTypeWithRef: SerializedSimpleTypeWithRef) => void; + getIdFromType: (simpleType: SimpleType) => number | undefined; + assignIdToType: (simpleType: SimpleType) => number; + } +): number { + // If this SimpleType already has been assigned an ID, we don't need to serialize it again + const existingId = getIdFromType(simpleType); + if (existingId != null) { + return existingId; + } + + const id = assignIdToType(simpleType); + + const serializedType = convertObject({ ...simpleType }, obj => { + // Replace with id whenever encountering a SimpleType + if (isSimpleType(obj)) { + // Convert the SimpleType recursively + const id = serializeTypeInternal(obj, { emitType, getIdFromType, assignIdToType }); + return `${TYPE_REF_PREFIX}${id}`; + } + return; + }); + + // Emit this serialized type to the type map + emitType(id, serializedType); + + return id; +} + +function convertObject(input: T, convert: (obj: unknown) => unknown): U { + let outer = true; + function convertObjectInner(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj.map(o => convertObjectInner(o)); + } + + if (!outer) { + const convertedObj = convert(obj); + if (convertedObj != null) { + return convertedObj; + } + } + + outer = false; + + if (typeof obj === "object" && obj != null) { + const newObj: { [key: string]: unknown } = {}; + for (const [key, value] of Object.entries(obj)) { + newObj[key] = convertObjectInner(value); + } + return newObj; + } + + return obj; + } + + return convertObjectInner(input) as U; +} diff --git a/packages/ts-simple-type/src/transform/simple-type-to-string.ts b/packages/ts-simple-type/src/transform/simple-type-to-string.ts new file mode 100644 index 00000000..14bb4834 --- /dev/null +++ b/packages/ts-simple-type/src/transform/simple-type-to-string.ts @@ -0,0 +1,175 @@ +import type { SimpleType, SimpleTypeFunctionParameter } from "../simple-type"; +import { isSimpleTypePrimitive } from "../simple-type"; + +/** + * Converts a simple type to a string. + * @param type Simple Type + */ +export function simpleTypeToString(type: SimpleType): string { + return simpleTypeToStringInternal(type, new Set()); +} + +function simpleTypeToStringInternal(type: SimpleType, visitTypeSet: Set): string { + if (!isSimpleTypePrimitive(type)) { + if (visitTypeSet.has(type)) { + return ""; + } + visitTypeSet = new Set([...visitTypeSet, type]); + } + + switch (type.kind) { + case "BOOLEAN_LITERAL": + return String(type.value); + case "NUMBER_LITERAL": + return String(type.value); + case "STRING_LITERAL": + return `"${type.value}"`; + case "BIG_INT_LITERAL": + return `${type.value}n`; + case "ES_SYMBOL": + return `Symbol()`; + case "ES_SYMBOL_UNIQUE": + return `Symbol(${type.name})`; + case "STRING": + return "string"; + case "BOOLEAN": + return "boolean"; + case "NUMBER": + return "number"; + case "BIG_INT": + return "bigint"; + case "UNDEFINED": + return "undefined"; + case "NULL": + return "null"; + case "ANY": + return "any"; + case "UNKNOWN": + return "unknown"; + case "VOID": + return "void"; + case "NEVER": + return "never"; + case "FUNCTION": + case "METHOD": { + if (type.kind === "FUNCTION" && type.name != null) return type.name; + const argText = functionArgTypesToString(type.parameters || [], visitTypeSet); + return `${type.typeParameters != null ? `<${type.typeParameters.map(tp => tp.name).join(",")}>` : ""}(${argText})${ + type.returnType != null ? ` => ${simpleTypeToStringInternal(type.returnType, visitTypeSet)}` : "" + }`; + } + case "ARRAY": { + const hasMultipleTypes = ["UNION", "INTERSECTION"].includes(type.type.kind); + let memberType = simpleTypeToStringInternal(type.type, visitTypeSet); + if (type.name != null && ["ArrayLike", "ReadonlyArray"].includes(type.name)) return `${type.name}<${memberType}>`; + if (hasMultipleTypes && type.type.name == null) memberType = `(${memberType})`; + return `${memberType}[]`; + } + case "UNION": { + if (type.name != null) return type.name; + return truncateAndJoinList( + type.types.map(t => simpleTypeToStringInternal(t, visitTypeSet)), + " | ", + { maxContentLength: 200 } + ); + } + case "ENUM": + return type.name; + case "ENUM_MEMBER": + return type.fullName; + case "INTERSECTION": + if (type.name != null) return type.name; + return truncateAndJoinList( + type.types.map(t => simpleTypeToStringInternal(t, visitTypeSet)), + " & ", + { maxContentLength: 200 } + ); + // @ts-expect-error Fallthrough case is intentional + case "INTERFACE": + if (type.name != null) return type.name; + // this fallthrough is intentional + case "OBJECT": { + if (type.members == null || type.members.length === 0) { + if (type.call == null && type.ctor == null) { + return "{}"; + } + + if (type.call != null && type.ctor == null) { + return simpleTypeToStringInternal(type.call, visitTypeSet); + } + } + + const entries: string[] = (type.members || []).map(member => { + // this check needs to change in the future + if (member.type.kind === "FUNCTION" || member.type.kind === "METHOD") { + const result = simpleTypeToStringInternal(member.type, visitTypeSet); + return `${member.name}${result.replace(" => ", ": ")}`; + } + + return `${member.name}: ${simpleTypeToStringInternal(member.type, visitTypeSet)}`; + }); + + if (type.ctor != null) { + entries.push(`new${simpleTypeToStringInternal(type.ctor, visitTypeSet)}`); + } + + if (type.call != null) { + entries.push(simpleTypeToStringInternal(type.call, visitTypeSet)); + } + + return `{ ${entries.join("; ")}${entries.length > 0 ? ";" : ""} }`; + } + case "TUPLE": + return `[${type.members.map(member => `${simpleTypeToStringInternal(member.type, visitTypeSet)}${member.optional ? "?" : ""}`).join(", ")}]`; + case "GENERIC_ARGUMENTS": { + const { target, typeArguments } = type; + return typeArguments.length === 0 + ? target.name || "" + : `${target.name}<${typeArguments.map(t => simpleTypeToStringInternal(t, visitTypeSet)).join(", ")}>`; + } + case "PROMISE": + return `${type.name || "Promise"}<${simpleTypeToStringInternal(type.type, visitTypeSet)}>`; + case "DATE": + return "Date"; + default: + return type.name || ""; + } +} + +function truncateAndJoinList( + items: string[], + combine: string, + { maxLength, maxContentLength }: { maxLength?: number; maxContentLength?: number } +): string { + const text = items.join(combine); + + // Truncate if too long + let slice = 0; + if (maxContentLength != null && text.length > maxContentLength) { + let curLength = 0; + for (const item of items) { + curLength += item.length; + slice++; + + if (curLength > maxContentLength) { + break; + } + } + } else if (maxLength != null && items.length > maxLength) { + slice = maxLength; + } + + if (slice !== 0) { + return [...items.slice(0, slice), `... ${items.length - slice} more ...`].join(combine); + } + + return text; +} + +function functionArgTypesToString(argTypes: SimpleTypeFunctionParameter[], visitTypeSet: Set): string { + return argTypes + .map(arg => { + return `${arg.rest ? "..." : ""}${arg.name}${arg.optional ? "?" : ""}: ${simpleTypeToStringInternal(arg.type, visitTypeSet)}`; + }) + .join(", "); +} diff --git a/packages/ts-simple-type/src/transform/to-simple-type.ts b/packages/ts-simple-type/src/transform/to-simple-type.ts new file mode 100644 index 00000000..711a9a74 --- /dev/null +++ b/packages/ts-simple-type/src/transform/to-simple-type.ts @@ -0,0 +1,676 @@ +import type * as tsModule from "typescript"; +import type { Declaration, Node, Signature, SignatureDeclaration, Symbol as ESSymbol, Type, TypeChecker } from "typescript"; +import { inspect } from "util"; +import { DEFAULT_TYPE_CACHE } from "../constants"; +import type { + SimpleType, + SimpleTypeAlias, + SimpleTypeEnumMember, + SimpleTypeFunction, + SimpleTypeFunctionParameter, + SimpleTypeGenericParameter, + SimpleTypeInterface, + SimpleTypeLiteral, + SimpleTypeMemberNamed, + SimpleTypeMethod, + SimpleTypeObject +} from "../simple-type"; +import { isSimpleType } from "../simple-type"; +import { getTypescriptModule } from "../ts-module"; +import { simplifySimpleTypes } from "../utils/simple-type-util"; +import { + getDeclaration, + getModifiersFromDeclaration, + getTypeArguments, + isArray, + isBigInt, + isBigIntLiteral, + isBoolean, + isBooleanLiteral, + isDate, + isEnum, + isESSymbolLike, + isFunction, + isImplicitGeneric, + isLiteral, + isMethod, + isMethodSignature, + isNever, + isNode, + isNonPrimitive, + isNull, + isNumber, + isObject, + isObjectTypeReference, + isPromise, + isString, + isSymbol, + isThisType, + isTupleTypeReference, + isUndefined, + isUniqueESSymbol, + isUnknown, + isVoid +} from "../utils/ts-util"; + +export interface ToSimpleTypeOptions { + eager?: boolean; + cache?: WeakMap; +} + +interface ToSimpleTypeInternalOptions { + cache: WeakMap; + checker: TypeChecker; + ts: typeof tsModule; + eager?: boolean; +} + +/** + * Converts a Typescript type to a "SimpleType" + * @param type The type to convert. + * @param checker + * @param options + */ +export function toSimpleType(type: SimpleType, checker?: TypeChecker, options?: ToSimpleTypeOptions): SimpleType; +export function toSimpleType(type: Node, checker: TypeChecker, options?: ToSimpleTypeOptions): SimpleType; +export function toSimpleType(type: Type, checker: TypeChecker, options?: ToSimpleTypeOptions): SimpleType; +export function toSimpleType(type: Type | Node | SimpleType, checker: TypeChecker, options?: ToSimpleTypeOptions): SimpleType; +export function toSimpleType(type: Type | Node | SimpleType, checker?: TypeChecker, options: ToSimpleTypeOptions = {}): SimpleType { + if (isSimpleType(type)) { + return type; + } + + checker = checker!; + + if (isNode(type)) { + // "type" is a "Node", convert it to a "Type" and continue. + return toSimpleType(checker.getTypeAtLocation(type), checker); + } + + return toSimpleTypeCached(type, { + checker, + eager: options.eager, + cache: options.cache || DEFAULT_TYPE_CACHE, + ts: getTypescriptModule() + }); +} + +function toSimpleTypeCached(type: Type, options: ToSimpleTypeInternalOptions): SimpleType { + if (options.cache.has(type)) { + return options.cache.get(type)!; + } + + // This function will resolve the type and assign the content to "target". + // This way we can cache "target" before calling "toSimpleTypeInternal" recursively + const resolveType = (target: SimpleType): void => { + // Construct the simple type recursively + //const simpleTypeOverwrite = options.cache.has(type) ? options.cache.get(type)! : toSimpleTypeInternal(type, options); + const simpleTypeOverwrite = toSimpleTypeInternal(type, options); + + // Strip undefined keys to make the output cleaner + Object.entries(simpleTypeOverwrite).forEach(([k, v]) => { + if (v == null) delete simpleTypeOverwrite[k as keyof typeof simpleTypeOverwrite]; + }); + + // Transfer properties on the simpleType to the placeholder + // This makes it possible to keep on using the reference "placeholder". + Object.assign(target, simpleTypeOverwrite); + }; + + if (options.eager === true) { + // Make and cache placeholder + const placeholder = {} as SimpleType; + options.cache.set(type, placeholder); + + // Resolve type into placeholder + resolveType(placeholder); + + Object.freeze(placeholder); + return placeholder; + } else { + const placeholder = {} as SimpleType; + + // A function that only resolves the type once + let didResolve = false; + const ensureResolved = () => { + if (!didResolve) { + resolveType(placeholder); + didResolve = true; + } + }; + + // Use "toStringTag" as a hook into resolving the type. + // If we don't have this hook, console.log would always print "{}" because the type hasn't been resolved + Object.defineProperty(placeholder, Symbol.toStringTag, { + get(): string { + resolveType(placeholder); + // Don't return any tag. Only use this function as a hook for calling "resolveType" + return undefined as never; + } + }); + + // Return a proxy with the purpose of resolving the type lazy + const proxy = new Proxy(placeholder, { + ownKeys(target: SimpleType) { + ensureResolved(); + return [...Object.getOwnPropertyNames(target), ...Object.getOwnPropertySymbols(target)]; + }, + has(target: SimpleType, p: PropertyKey) { + // Always return true if we test for "kind", but don't resolve the type + // This way "isSimpleType" (which checks for "kind") will succeed without resolving the type + if (p === "kind") { + return true; + } + + ensureResolved(); + return p in target; + }, + getOwnPropertyDescriptor(target: SimpleType, p: keyof SimpleType) { + ensureResolved(); + return Object.getOwnPropertyDescriptor(target, p); + }, + get: (target: SimpleType, p: keyof SimpleType) => { + ensureResolved(); + return target[p]; + }, + set: (target: SimpleType, p: keyof SimpleType) => { + throw new TypeError(`Cannot assign to read only property '${p}'`); + } + }); + + options.cache.set(type, proxy); + + return proxy; + } +} + +/** + * Tries to lift a potential generic type and wrap the result in a "GENERIC_ARGUMENTS" simple type and/or "ALIAS" type. + * Returns the "simpleType" otherwise. + * @param simpleType + * @param type + * @param options + */ +function liftGenericType( + type: Type, + options: ToSimpleTypeInternalOptions +): { generic: (target: SimpleType) => SimpleType; target: Type } | undefined { + // Check for alias reference + if (type.aliasSymbol != null) { + const aliasDeclaration = getDeclaration(type.aliasSymbol, options.ts); + const typeParameters = getTypeParameters(aliasDeclaration, options); + + return { + target: type, + generic: target => { + // Lift the simple type to an ALIAS type. + const aliasType: SimpleTypeAlias = { + kind: "ALIAS", + name: type.aliasSymbol!.getName() || "", + target, + typeParameters + }; + + // Lift the alias type if it uses generic arguments. + if (type.aliasTypeArguments != null) { + const typeArguments = Array.from(type.aliasTypeArguments || []).map(t => toSimpleTypeCached(t, options)); + + return { + kind: "GENERIC_ARGUMENTS", + target: aliasType, + typeArguments + }; + } + + return target; + } + }; + } + + // Check if the type is a generic interface/class reference and lift it. + else if (isObject(type, options.ts) && isObjectTypeReference(type, options.ts) && type.typeArguments != null && type.typeArguments.length > 0) { + // Special case for array, tuple and promise, they are generic in themselves + if (isImplicitGeneric(type, options.checker, options.ts)) { + return undefined; + } + + return { + target: type.target, + generic: target => { + const typeArguments = Array.from(type.typeArguments || []).map(t => toSimpleTypeCached(t, options)); + + return { + kind: "GENERIC_ARGUMENTS", + target, + typeArguments + }; + } + }; + } + + return undefined; +} + +function toSimpleTypeInternal(type: Type, options: ToSimpleTypeInternalOptions): SimpleType { + const { checker, ts } = options; + + const symbol: ESSymbol | undefined = type.getSymbol(); + const name = symbol != null ? getRealSymbolName(symbol, ts) : undefined; + + let simpleType: SimpleType | undefined; + + const generic = liftGenericType(type, options); + if (generic != null) { + type = generic.target; + } + + if (isLiteral(type, ts)) { + const literalSimpleType = primitiveLiteralToSimpleType(type, checker, ts); + if (literalSimpleType != null) { + // Enum members + if (symbol != null && symbol.flags & ts.SymbolFlags.EnumMember) { + const parentSymbol = (symbol as ESSymbol & { parent: ESSymbol | undefined }).parent; + + if (parentSymbol != null) { + return { + name: name || "", + fullName: `${parentSymbol.name}.${name}`, + kind: "ENUM_MEMBER", + type: literalSimpleType + }; + } + } + + // Literals types + return literalSimpleType; + } + } + + // Primitive types + else if (isString(type, ts)) { + simpleType = { kind: "STRING", name }; + } else if (isNumber(type, ts)) { + simpleType = { kind: "NUMBER", name }; + } else if (isBoolean(type, ts)) { + simpleType = { kind: "BOOLEAN", name }; + } else if (isBigInt(type, ts)) { + simpleType = { kind: "BIG_INT", name }; + } else if (isESSymbolLike(type, ts)) { + simpleType = { kind: "ES_SYMBOL", name }; + } else if (isUndefined(type, ts)) { + simpleType = { kind: "UNDEFINED", name }; + } else if (isNull(type, ts)) { + simpleType = { kind: "NULL", name }; + } else if (isUnknown(type, ts)) { + simpleType = { kind: "UNKNOWN", name }; + } else if (isVoid(type, ts)) { + simpleType = { kind: "VOID", name }; + } else if (isNever(type, ts)) { + simpleType = { kind: "NEVER", name }; + } + + // Enum + else if (isEnum(type, ts) && type.isUnion()) { + simpleType = { + name: name || "", + kind: "ENUM", + types: type.types.map(t => toSimpleTypeCached(t, options) as SimpleTypeEnumMember) + }; + } + + // Promise + else if (isPromise(type, checker, ts)) { + simpleType = { + kind: "PROMISE", + name, + type: toSimpleTypeCached(getTypeArguments(type, checker, ts)[0], options) + }; + } + + // Unions and intersections + else if (type.isUnion()) { + simpleType = { + kind: "UNION", + types: simplifySimpleTypes(type.types.map(t => toSimpleTypeCached(t, options))), + name + }; + } else if (type.isIntersection()) { + simpleType = { + kind: "INTERSECTION", + types: simplifySimpleTypes(type.types.map(t => toSimpleTypeCached(t, options))), + name + }; + } + + // Date + else if (isDate(type, ts)) { + simpleType = { + kind: "DATE", + name + }; + } + + // Array + else if (isArray(type, checker, ts)) { + simpleType = { + kind: "ARRAY", + type: toSimpleTypeCached(getTypeArguments(type, checker, ts)[0], options), + name + }; + } else if (isTupleTypeReference(type, ts)) { + const types = getTypeArguments(type, checker, ts); + + const minLength = type.target.minLength; + + simpleType = { + kind: "TUPLE", + rest: type.target.hasRestElement || false, + members: types.map((childType, i) => { + return { + optional: i >= minLength, + type: toSimpleTypeCached(childType, options) + }; + }), + name + }; + } + + // Method signatures + else if (isMethodSignature(type, ts)) { + const callSignatures = type.getCallSignatures(); + simpleType = getSimpleFunctionFromCallSignatures(callSignatures, options); + } + + // Class + else if (type.isClass() && symbol != null) { + const classDecl = getDeclaration(symbol, ts); + + if (classDecl != null && ts.isClassDeclaration(classDecl)) { + const ctor = (() => { + const ctorSymbol = symbol != null && symbol.members != null ? symbol.members.get("__constructor" as never) : undefined; + if (ctorSymbol != null && symbol != null) { + const ctorDecl = + ctorSymbol.declarations !== undefined && ctorSymbol.declarations?.length > 0 ? ctorSymbol.declarations[0] : ctorSymbol.valueDeclaration; + + if (ctorDecl != null && ts.isConstructorDeclaration(ctorDecl)) { + return getSimpleFunctionFromSignatureDeclaration(ctorDecl, options) as SimpleTypeFunction; + } + } + return; + })(); + + const call = getSimpleFunctionFromCallSignatures(type.getCallSignatures(), options) as SimpleTypeFunction; + + const members = checker + .getPropertiesOfType(type) + .map(symbol => { + const declaration = getDeclaration(symbol, ts); + + // Some instance properties may have an undefined declaration. + // Since we can't do too much without a declaration, filtering + // these out seems like the best strategy for the moment. + // + // See https://github.com/runem/web-component-analyzer/issues/60 for + // more info. + if (declaration == null) return null; + + return { + name: symbol.name, + optional: (symbol.flags & ts.SymbolFlags.Optional) !== 0, + modifiers: getModifiersFromDeclaration(declaration, ts), + type: toSimpleTypeCached(checker.getTypeAtLocation(declaration), options) + } as SimpleTypeMemberNamed; + }) + .filter((member): member is NonNullable => member != null); + + const typeParameters = getTypeParameters(getDeclaration(symbol, ts), options); + + simpleType = { + kind: "CLASS", + name, + call, + ctor, + typeParameters, + members + }; + } + } + + // Interface + else if ((type.isClassOrInterface() || isObject(type, ts)) && !(symbol?.name === "Function")) { + // Handle the empty object + if (isObject(type, ts) && symbol?.name === "Object") { + return { + kind: "OBJECT" + }; + } + + const members = type.getProperties().map(symbol => { + const declaration = getDeclaration(symbol, ts); + + return { + name: symbol.name, + optional: (symbol.flags & ts.SymbolFlags.Optional) !== 0, + modifiers: declaration != null ? getModifiersFromDeclaration(declaration, ts) : [], + type: toSimpleTypeCached(checker.getTypeAtLocation(symbol.valueDeclaration!), options) + }; + }); + + const ctor = getSimpleFunctionFromCallSignatures(type.getConstructSignatures(), options) as SimpleTypeFunction; + + const call = getSimpleFunctionFromCallSignatures(type.getCallSignatures(), options) as SimpleTypeFunction; + + const typeParameters = + (type.isClassOrInterface() && type.typeParameters != null + ? type.typeParameters.map(t => toSimpleTypeCached(t, options) as SimpleTypeGenericParameter) + : undefined) || (symbol != null ? getTypeParameters(getDeclaration(symbol, ts), options) : undefined); + + let indexType: SimpleTypeInterface["indexType"] = {}; + if (type.getStringIndexType()) { + indexType["STRING"] = toSimpleTypeCached(type.getStringIndexType()!, options); + } + if (type.getNumberIndexType()) { + indexType["NUMBER"] = toSimpleTypeCached(type.getNumberIndexType()!, options); + } + if (Object.keys(indexType).length === 0) { + indexType = undefined; + } + + // Simplify: if there is only a single "call" signature and nothing else, just return the call signature + /*if (call != null && members.length === 0 && ctor == null && indexType == null) { + return { ...call, name, typeParameters }; + }*/ + + simpleType = { + kind: type.isClassOrInterface() ? "INTERFACE" : "OBJECT", + typeParameters, + ctor, + members, + name, + indexType, + call + } as SimpleTypeInterface | SimpleTypeObject; + } + + // Handle "object" type + else if (isNonPrimitive(type, ts)) { + return { + kind: "NON_PRIMITIVE" + }; + } + + // Function + else if (symbol != null && (isFunction(type, ts) || isMethod(type, ts))) { + simpleType = getSimpleFunctionFromCallSignatures(type.getCallSignatures(), options, name); + + if (simpleType == null) { + simpleType = { + kind: "FUNCTION", + name + }; + } + } + + // Type Parameter + else if (type.isTypeParameter() && symbol != null) { + // This type + if (isThisType(type, ts) && symbol.valueDeclaration != null) { + return toSimpleTypeCached(checker.getTypeAtLocation(symbol.valueDeclaration), options); + } + + const defaultType = type.getDefault(); + const defaultSimpleType = defaultType != null ? toSimpleTypeCached(defaultType, options) : undefined; + + simpleType = { + kind: "GENERIC_PARAMETER", + name: symbol.getName(), + default: defaultSimpleType + } as SimpleTypeGenericParameter; + } + + // If no type was found, return "ANY" + if (simpleType == null) { + simpleType = { + kind: "ANY", + name + }; + } + + // Lift generic types and aliases if possible + if (generic != null) { + return generic.generic(simpleType); + } + + return simpleType; +} + +function primitiveLiteralToSimpleType(type: Type, checker: TypeChecker, ts: typeof tsModule): SimpleTypeLiteral | undefined { + if (type.isNumberLiteral()) { + return { + kind: "NUMBER_LITERAL", + value: type.value + }; + } else if (type.isStringLiteral()) { + return { + kind: "STRING_LITERAL", + value: type.value + }; + } else if (isBooleanLiteral(type, ts)) { + // See https://github.com/Microsoft/TypeScript/issues/22269 for more information + return { + kind: "BOOLEAN_LITERAL", + value: checker.typeToString(type) === "true" + }; + } else if (isBigIntLiteral(type, ts)) { + return { + kind: "BIG_INT_LITERAL", + /* global BigInt */ + value: BigInt(`${type.value.negative ? "-" : ""}${type.value.base10Value}`) + }; + } else if (isUniqueESSymbol(type, ts)) { + return { + kind: "ES_SYMBOL_UNIQUE", + value: String(type.escapedName) || Math.floor(Math.random() * 100000000).toString() + }; + } + return; +} + +function getSimpleFunctionFromCallSignatures( + signatures: readonly Signature[], + options: ToSimpleTypeInternalOptions, + fallbackName?: string +): SimpleTypeFunction | SimpleTypeMethod | undefined { + if (signatures.length === 0) { + return undefined; + } + + const signature = signatures[signatures.length - 1]; + + const signatureDeclaration = signature.getDeclaration(); + + // According to the types the signatureDeclaration should always have a value, but there was an error after upgrading to a newer TS (~5.7) version that was getting thrown because it returned undefined + // The error was coming from the `infinity.spec.ts` test + if (!signatureDeclaration) return undefined; + + return getSimpleFunctionFromSignatureDeclaration(signatureDeclaration, options, fallbackName); +} + +function getSimpleFunctionFromSignatureDeclaration( + signatureDeclaration: SignatureDeclaration, + options: ToSimpleTypeInternalOptions, + fallbackName?: string +): SimpleTypeFunction | SimpleTypeMethod | undefined { + const { checker } = options; + + const symbol = checker.getSymbolAtLocation(signatureDeclaration); + + const parameters = signatureDeclaration.parameters.map(parameterDecl => { + const argType = checker.getTypeAtLocation(parameterDecl); + + return { + name: parameterDecl.name.getText() || fallbackName, + optional: parameterDecl.questionToken != null, + type: toSimpleTypeCached(argType, options), + rest: parameterDecl.dotDotDotToken != null, + initializer: parameterDecl.initializer != null + } as SimpleTypeFunctionParameter; + }); + + const name = symbol != null ? symbol.getName() : undefined; + + const type = checker.getTypeAtLocation(signatureDeclaration); + + const kind = isMethod(type, options.ts) ? "METHOD" : "FUNCTION"; + + const signature = checker.getSignatureFromDeclaration(signatureDeclaration); + + const returnType = signature == null ? undefined : toSimpleTypeCached(checker.getReturnTypeOfSignature(signature), options); + + const typeParameters = getTypeParameters(signatureDeclaration, options); + + return { name, kind, returnType, parameters, typeParameters } as SimpleTypeFunction | SimpleTypeMethod; +} + +function getRealSymbolName(symbol: ESSymbol, ts: typeof tsModule): string | undefined { + const name = symbol.getName(); + if (name != null && [ts.InternalSymbolName.Type, ts.InternalSymbolName.Object, ts.InternalSymbolName.Function].includes(name as never)) { + return undefined; + } + + return name; +} + +function getTypeParameters(obj: ESSymbol | Declaration | undefined, options: ToSimpleTypeInternalOptions): SimpleTypeGenericParameter[] | undefined { + if (obj == null) return undefined; + + if (isSymbol(obj)) { + const decl = getDeclaration(obj, options.ts); + return getTypeParameters(decl, options); + } + + if ( + options.ts.isClassDeclaration(obj) || + options.ts.isFunctionDeclaration(obj) || + options.ts.isFunctionTypeNode(obj) || + options.ts.isTypeAliasDeclaration(obj) || + options.ts.isMethodDeclaration(obj) || + options.ts.isMethodSignature(obj) + ) { + return obj.typeParameters == null + ? undefined + : Array.from(obj.typeParameters) + .map(td => options.checker.getTypeAtLocation(td)) + .map(t => toSimpleTypeCached(t, options) as SimpleTypeGenericParameter); + } + + return undefined; +} + +// @ts-expect-error - This is unused, but useful for debugging +function log(input: unknown, d = 3) { + const str = inspect(input, { depth: d, colors: true }); + + // eslint-disable-next-line no-console + console.log(str.replace(/checker: {[\s\S]*?}/g, "")); +} diff --git a/packages/ts-simple-type/src/transform/type-to-string.ts b/packages/ts-simple-type/src/transform/type-to-string.ts new file mode 100644 index 00000000..98495ee1 --- /dev/null +++ b/packages/ts-simple-type/src/transform/type-to-string.ts @@ -0,0 +1,19 @@ +import type { Type, TypeChecker } from "typescript"; +import type { SimpleType } from "../simple-type"; +import { isSimpleType } from "../simple-type"; +import { simpleTypeToString } from "./simple-type-to-string"; + +/** + * Returns a string representation of a given type. + * @param simpleType + */ +export function typeToString(simpleType: SimpleType): string; +export function typeToString(type: SimpleType | Type, checker: TypeChecker): string; +export function typeToString(type: SimpleType | Type, checker?: TypeChecker): string { + if (isSimpleType(type)) { + return simpleTypeToString(type); + } + + // Use the typescript checker to return a string for a type + return checker!.typeToString(type); +} diff --git a/packages/ts-simple-type/src/ts-module.ts b/packages/ts-simple-type/src/ts-module.ts new file mode 100644 index 00000000..a34fedd3 --- /dev/null +++ b/packages/ts-simple-type/src/ts-module.ts @@ -0,0 +1,11 @@ +import * as tsModule from "typescript"; + +let selectedTSModule = tsModule; + +export function setTypescriptModule(ts: typeof tsModule) { + selectedTSModule = ts; +} + +export function getTypescriptModule(): typeof tsModule { + return selectedTSModule; +} diff --git a/packages/ts-simple-type/src/utils/list-util.ts b/packages/ts-simple-type/src/utils/list-util.ts new file mode 100644 index 00000000..3b974543 --- /dev/null +++ b/packages/ts-simple-type/src/utils/list-util.ts @@ -0,0 +1,16 @@ +export function or(list: T[], match: (arg: T, i: number) => boolean): boolean { + return list.find((a, i) => match(a, i)) != null; +} + +export function and(list: T[], match: (arg: T, i: number) => boolean): boolean { + return list.find((a, i) => !match(a, i)) == null; +} + +export function zip(listA: T[], listB: U[]): [T, U][] | null { + if (listA.length !== listB.length) return null; + return listA.map((a, i) => [a, listB[i]] as [T, U]); +} + +export function flat(list: T[][]): T[] { + return Array.prototype.concat.apply([], list); +} diff --git a/packages/ts-simple-type/src/utils/resolve-type.ts b/packages/ts-simple-type/src/utils/resolve-type.ts new file mode 100644 index 00000000..c6025a68 --- /dev/null +++ b/packages/ts-simple-type/src/utils/resolve-type.ts @@ -0,0 +1,21 @@ +import { DEFAULT_GENERIC_PARAMETER_TYPE } from "../constants"; +import type { SimpleType, SimpleTypeGenericArguments, SimpleTypeGenericParameter } from "../simple-type"; +import { extendTypeParameterMap } from "./simple-type-util"; + +export function resolveType( + simpleType: SimpleType, + parameterMap: Map = new Map() +): Exclude { + switch (simpleType.kind) { + case "GENERIC_PARAMETER": { + const resolvedArgument = parameterMap?.get(simpleType.name); + return resolveType(resolvedArgument || /*simpleType.default ||*/ DEFAULT_GENERIC_PARAMETER_TYPE, parameterMap); + } + case "GENERIC_ARGUMENTS": { + const updatedGenericParameterMap = extendTypeParameterMap(simpleType, parameterMap); + return resolveType(simpleType.target, updatedGenericParameterMap); + } + default: + return simpleType; + } +} diff --git a/packages/ts-simple-type/src/utils/simple-type-util.ts b/packages/ts-simple-type/src/utils/simple-type-util.ts new file mode 100644 index 00000000..ae8e10b2 --- /dev/null +++ b/packages/ts-simple-type/src/utils/simple-type-util.ts @@ -0,0 +1,103 @@ +import { DEFAULT_GENERIC_PARAMETER_TYPE } from "../constants"; +import type { + SimpleType, + SimpleTypeBooleanLiteral, + SimpleTypeGenericArguments, + SimpleTypeNull, + SimpleTypeNumberLiteral, + SimpleTypeTuple, + SimpleTypeUndefined +} from "../simple-type"; +import { isSimpleTypeLiteral, PRIMITIVE_TYPE_KINDS } from "../simple-type"; +import { resolveType } from "./resolve-type"; + +/** + * Returns a type that represents the length of the Tuple type + * Read more here: https://github.com/microsoft/TypeScript/pull/24897 + * @param tuple + */ +export function getTupleLengthType(tuple: SimpleTypeTuple): SimpleType { + // When the tuple has rest argument, return "number" + if (tuple.rest) { + return { + kind: "NUMBER" + }; + } + + // Else return an intersection of number literals that represents all possible lengths + const minLength = tuple.members.filter(member => !member.optional).length; + + if (minLength === tuple.members.length) { + return { + kind: "NUMBER_LITERAL", + value: minLength + }; + } + + return { + kind: "UNION", + types: new Array(tuple.members.length - minLength + 1).fill(0).map( + (_, i) => + ({ + kind: "NUMBER_LITERAL", + value: minLength + i + }) as SimpleTypeNumberLiteral + ) + }; +} + +export function simplifySimpleTypes(types: SimpleType[]): SimpleType[] { + let newTypes: SimpleType[] = [...types]; + const NULLABLE_TYPE_KINDS = ["UNDEFINED", "NULL"]; + + // Only include one instance of primitives and literals + newTypes = newTypes.filter((type, i) => { + // Only include one of each literal with specific value + if (isSimpleTypeLiteral(type)) { + return !newTypes.slice(0, i).some(newType => newType.kind === type.kind && newType.value === type.value); + } + + if (PRIMITIVE_TYPE_KINDS.includes(type.kind) || NULLABLE_TYPE_KINDS.includes(type.kind)) { + // Remove this type from the array if there is already a primitive in the array + return !newTypes.slice(0, i).some(t => t.kind === type.kind); + } + + return true; + }); + + // Simplify boolean literals + const booleanLiteralTypes = newTypes.filter((t): t is SimpleTypeBooleanLiteral => t.kind === "BOOLEAN_LITERAL"); + if (booleanLiteralTypes.find(t => t.value === true) != null && booleanLiteralTypes.find(t => t.value === false) != null) { + newTypes = [...newTypes.filter(type => type.kind !== "BOOLEAN_LITERAL"), { kind: "BOOLEAN" }]; + } + + // Reorder "NULL" and "UNDEFINED" to be last + const nullableTypes = newTypes.filter((t): t is SimpleTypeUndefined | SimpleTypeNull => NULLABLE_TYPE_KINDS.includes(t.kind)); + if (nullableTypes.length > 0) { + newTypes = [ + ...newTypes.filter(t => !NULLABLE_TYPE_KINDS.includes(t.kind)), + ...nullableTypes.sort((t1, t2) => (t1.kind === "NULL" ? (t2.kind === "UNDEFINED" ? -1 : 0) : t2.kind === "NULL" ? 1 : 0)) + ]; + } + + return newTypes; +} + +export function extendTypeParameterMap(genericType: SimpleTypeGenericArguments, existingMap: Map) { + const target = resolveType(genericType.target, existingMap); + + if ("typeParameters" in target) { + const parameterEntries = (target.typeParameters || []).map((parameter, i) => { + const typeArg = genericType.typeArguments[i]; + const resolvedTypeArg = typeArg == null ? /*parameter.default || */ DEFAULT_GENERIC_PARAMETER_TYPE : resolveType(typeArg, existingMap); + + //return [parameter.name, genericType.typeArguments[i] || parameter.default || { kind: "ANY" }] as [string, SimpleType]; + return [parameter.name, resolvedTypeArg] as [string, SimpleType]; + }); + const allParameterEntries = [...existingMap.entries(), ...parameterEntries]; + + return new Map(allParameterEntries); + } + + return existingMap; +} diff --git a/packages/ts-simple-type/src/utils/ts-util.ts b/packages/ts-simple-type/src/utils/ts-util.ts new file mode 100644 index 00000000..eeabea08 --- /dev/null +++ b/packages/ts-simple-type/src/utils/ts-util.ts @@ -0,0 +1,250 @@ +import type * as tsModule from "typescript"; +import type { + BigIntLiteralType, + Declaration, + GenericType, + LiteralType, + Node, + ObjectType, + Program, + Symbol, + TupleTypeReference, + Type, + TypeChecker, + TypeFlags, + TypeReference, + UniqueESSymbolType +} from "typescript"; +import type { SimpleTypeModifierKind } from "../simple-type"; +import { and, or } from "./list-util"; + +export function isTypeChecker(obj: unknown): obj is TypeChecker { + return obj != null && typeof obj === "object" && "getSymbolAtLocation" in obj!; +} + +export function isProgram(obj: unknown): obj is Program { + return obj != null && typeof obj === "object" && "getTypeChecker" in obj! && "getCompilerOptions" in obj!; +} + +export function isNode(obj: unknown): obj is Node { + return obj != null && typeof obj === "object" && "kind" in obj! && "flags" in obj! && "pos" in obj! && "end" in obj!; +} + +function typeHasFlag(type: Type, flag: TypeFlags | TypeFlags[], op: "and" | "or" = "and"): boolean { + return hasFlag(type.flags, flag, op); +} + +function hasFlag(flags: number, flag: number | number[], op: "and" | "or" = "and"): boolean { + if (Array.isArray(flag)) { + return (op === "and" ? and : or)(flag, f => hasFlag(flags, f)); + } + + return (flags & flag) !== 0; +} + +export function isBoolean(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.BooleanLike) || type.symbol?.name === "Boolean"; +} + +export function isBooleanLiteral(type: Type, ts: typeof tsModule): type is LiteralType { + return typeHasFlag(type, ts.TypeFlags.BooleanLiteral); +} + +export function isBigIntLiteral(type: Type, ts: typeof tsModule): type is BigIntLiteralType { + return typeHasFlag(type, ts.TypeFlags.BigIntLiteral); +} + +export function isUniqueESSymbol(type: Type, ts: typeof tsModule): type is UniqueESSymbolType { + return typeHasFlag(type, ts.TypeFlags.UniqueESSymbol); +} + +export function isESSymbolLike(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.ESSymbolLike) || type.symbol?.name === "Symbol"; +} + +export function isLiteral(type: Type, ts: typeof tsModule): type is LiteralType { + return type.isLiteral() || isBooleanLiteral(type, ts) || isBigIntLiteral(type, ts) || isUniqueESSymbol(type, ts); +} + +export function isString(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.StringLike) || type.symbol?.name === "String"; +} + +export function isNumber(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.NumberLike) || type.symbol?.name === "Number"; +} + +export function isAny(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.Any); +} + +export function isEnum(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.EnumLike); +} + +export function isBigInt(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.BigIntLike) || type.symbol?.name === "BigInt"; +} + +export function isObject(type: Type, ts: typeof tsModule): type is ObjectType { + return typeHasFlag(type, ts.TypeFlags.Object) || type.symbol?.name === "Object"; +} + +export function isNonPrimitive(type: Type, ts: typeof tsModule): type is ObjectType { + return typeHasFlag(type, ts.TypeFlags.NonPrimitive) || type.symbol?.name === "object"; +} + +export function isThisType(type: Type, ts: typeof tsModule): type is ObjectType { + const kind = type.getSymbol()?.valueDeclaration?.kind; + if (kind == null) { + return false; + } + + return hasFlag(kind, ts.SyntaxKind.ThisKeyword); +} + +export function isUnknown(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.Unknown); +} + +export function isNull(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.Null); +} + +export function isUndefined(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.Undefined); +} + +export function isVoid(type: Type, ts: typeof tsModule) { + return typeHasFlag(type, ts.TypeFlags.VoidLike); +} + +export function isNever(type: Type, ts: typeof tsModule): boolean { + return typeHasFlag(type, ts.TypeFlags.Never); +} + +export function isObjectTypeReference(type: ObjectType, ts: typeof tsModule): type is TypeReference { + return hasFlag(type.objectFlags, ts.ObjectFlags.Reference); +} + +export function isSymbol(obj: object): obj is Symbol { + return "flags" in obj && "name" in obj && "getDeclarations" in obj; +} + +export function isType(obj: object): obj is Type { + return "flags" in obj && "getSymbol" in obj; +} + +export function isMethod(type: Type, ts: typeof tsModule): type is TypeReference { + if (!isObject(type, ts)) return false; + const symbol = type.getSymbol(); + if (symbol == null) return false; + + return hasFlag(symbol.flags, ts.SymbolFlags.Method); +} + +export function getDeclaration(symbol: Symbol, ts: typeof tsModule): Declaration | undefined { + const declarations = symbol.getDeclarations(); + if (declarations == null || declarations.length === 0) return symbol.valueDeclaration; + return declarations[0]; +} + +export function isArray(type: Type, checker: TypeChecker, ts: typeof tsModule): type is TypeReference { + if (!isObject(type, ts)) return false; + const symbol = type.getSymbol(); + if (symbol == null) return false; + return getTypeArguments(type, checker, ts).length === 1 && ["ArrayLike", "ReadonlyArray", "ConcatArray", "Array"].includes(symbol.getName()); +} + +export function isPromise(type: Type, checker: TypeChecker, ts: typeof tsModule): type is TypeReference { + if (!isObject(type, ts)) return false; + const symbol = type.getSymbol(); + if (symbol == null) return false; + return getTypeArguments(type, checker, ts).length === 1 && ["PromiseLike", "Promise"].includes(symbol.getName()); +} + +export function isDate(type: Type, ts: typeof tsModule): type is ObjectType { + if (!isObject(type, ts)) return false; + const symbol = type.getSymbol(); + if (symbol == null) return false; + return symbol.getName() === "Date"; +} + +export function isTupleTypeReference(type: Type, ts: typeof tsModule): type is TupleTypeReference { + const target = getTargetType(type, ts); + if (target == null) return false; + return (target.objectFlags & ts.ObjectFlags.Tuple) !== 0; +} + +export function isFunction(type: Type, ts: typeof tsModule): type is ObjectType { + if (!isObject(type, ts)) return false; + const symbol = type.getSymbol(); + if (symbol == null) return false; + return ( + (symbol.flags & ts.SymbolFlags.Function) !== 0 || + symbol.escapedName === "Function" || + (symbol.members != null && symbol.members.has("__call" as never)) + ); +} + +export function getTypeArguments(type: ObjectType, checker: TypeChecker, ts: typeof tsModule): Type[] { + if (isObject(type, ts)) { + if (isObjectTypeReference(type, ts)) { + if ("getTypeArguments" in checker) { + return Array.from(checker.getTypeArguments(type) || []); + } else { + return Array.from(type.typeArguments || []); + } + } + } + + return []; +} + +export function getTargetType(type: Type, ts: typeof tsModule): GenericType | undefined { + if (isObject(type, ts) && isObjectTypeReference(type, ts)) { + return type.target; + } + return; +} + +export function getModifiersFromDeclaration(declaration: Declaration, ts: typeof tsModule): SimpleTypeModifierKind[] { + const tsModifiers = ts.getCombinedModifierFlags(declaration); + const modifiers: SimpleTypeModifierKind[] = []; + + const map: Record = { + [ts.ModifierFlags.Export]: "EXPORT", + [ts.ModifierFlags.Ambient]: "AMBIENT", + [ts.ModifierFlags.Public]: "PUBLIC", + [ts.ModifierFlags.Private]: "PRIVATE", + [ts.ModifierFlags.Protected]: "PROTECTED", + [ts.ModifierFlags.Static]: "STATIC", + [ts.ModifierFlags.Readonly]: "READONLY", + [ts.ModifierFlags.Abstract]: "ABSTRACT", + [ts.ModifierFlags.Async]: "ASYNC", + [ts.ModifierFlags.Default]: "DEFAULT" + }; + + Object.entries(map).forEach(([tsModifier, modifierKind]) => { + if ((tsModifiers & Number(tsModifier)) !== 0) { + modifiers.push(modifierKind); + } + }); + + return modifiers; +} + +export function isImplicitGeneric(type: Type, checker: TypeChecker, ts: typeof tsModule): boolean { + return isArray(type, checker, ts) || isTupleTypeReference(type, ts) || isPromise(type, checker, ts); +} + +export function isMethodSignature(type: Type, ts: typeof tsModule): boolean { + const symbol = type.getSymbol(); + if (symbol == null) return false; + if (!isObject(type, ts)) return false; + if (type.getCallSignatures().length === 0) return false; + + const decl = getDeclaration(symbol, ts); + if (decl == null) return false; + return decl.kind === ts.SyntaxKind.MethodSignature; +} diff --git a/packages/ts-simple-type/src/utils/validate-type.ts b/packages/ts-simple-type/src/utils/validate-type.ts new file mode 100644 index 00000000..c0df5f80 --- /dev/null +++ b/packages/ts-simple-type/src/utils/validate-type.ts @@ -0,0 +1,47 @@ +import { DEFAULT_GENERIC_PARAMETER_TYPE } from "../constants"; +import type { SimpleType } from "../simple-type"; +import { and, or } from "./list-util"; +import { extendTypeParameterMap } from "./simple-type-util"; + +export function validateType(type: SimpleType, callback: (simpleType: SimpleType) => boolean | undefined | void): boolean { + return validateTypeInternal(type, callback, new Map()); +} + +function validateTypeInternal( + type: SimpleType, + callback: (simpleType: SimpleType) => boolean | undefined | void, + parameterMap: Map +): boolean { + const res = callback(type); + + if (res != null) { + return res; + } + + switch (type.kind) { + case "ENUM": + case "UNION": { + return or(type.types, childType => validateTypeInternal(childType, callback, parameterMap)); + } + + case "ALIAS": { + return validateTypeInternal(type.target, callback, parameterMap); + } + + case "INTERSECTION": { + return and(type.types, childType => validateTypeInternal(childType, callback, parameterMap)); + } + + case "GENERIC_PARAMETER": { + const resolvedArgument = parameterMap?.get(type.name); + return validateTypeInternal(resolvedArgument || DEFAULT_GENERIC_PARAMETER_TYPE, callback, parameterMap); + } + + case "GENERIC_ARGUMENTS": { + const updatedGenericParameterMap = extendTypeParameterMap(type, parameterMap); + return validateTypeInternal(type.target, callback, updatedGenericParameterMap); + } + } + + return false; +} diff --git a/packages/ts-simple-type/test/helpers/analyze-text.ts b/packages/ts-simple-type/test/helpers/analyze-text.ts new file mode 100644 index 00000000..be0b0b19 --- /dev/null +++ b/packages/ts-simple-type/test/helpers/analyze-text.ts @@ -0,0 +1,109 @@ +import { existsSync, readFileSync } from "fs"; +import { dirname, join } from "path"; +import type { CompilerOptions, Program, SourceFile } from "typescript"; +import { createProgram, createSourceFile, getDefaultLibFileName, ModuleKind, ScriptKind, ScriptTarget, sys } from "typescript"; + +export interface ITestFile { + fileName: string; + text: string; + entry?: boolean; +} + +export type TestFile = ITestFile | string; + +/** + * Creates a program using input code + * @param {ITestFile[]|TestFile} inputFiles + * @param options + */ +export function programWithVirtualFiles( + inputFiles: TestFile[] | TestFile, + { options, includeLib }: { options?: CompilerOptions; includeLib?: boolean } = {} +): Program { + const cwd = process.cwd(); + + const files: ITestFile[] = (Array.isArray(inputFiles) ? inputFiles : [inputFiles]) + .map(file => + typeof file === "string" + ? { + text: file, + fileName: `auto-generated-${Math.floor(Math.random() * 100000)}.ts`, + entry: true + } + : file + ) + .map(file => ({ ...file, fileName: join(cwd, file.fileName) })); + + const entryFile = files.find(file => file.entry === true) || files[0]; + if (entryFile == null) { + throw new ReferenceError(`No entry could be found`); + } + + const compilerOptions: CompilerOptions = { + module: ModuleKind.ESNext, + target: ScriptTarget.ESNext, + allowJs: true, + sourceMap: false, + noEmitOnError: true, + noImplicitAny: true, + strict: true, + ...options + }; + + return createProgram({ + rootNames: files.map(file => file.fileName), + options: compilerOptions, + host: { + writeFile: () => {}, + readFile: (fileName: string): string | undefined => { + const matchedFile = files.find(currentFile => currentFile.fileName === fileName); + if (matchedFile != null) { + return matchedFile.text; + } + + if (includeLib) { + fileName = fileName.match(/[/\\]/) ? fileName : join(dirname(require.resolve("typescript")), fileName); + } + + if (existsSync(fileName)) { + return readFileSync(fileName, "utf8").toString(); + } + + return undefined; + }, + fileExists: (fileName: string): boolean => { + return files.some(currentFile => currentFile.fileName === fileName); + }, + getSourceFile(fileName: string, languageVersion: ScriptTarget): SourceFile | undefined { + const sourceText = this.readFile(fileName); + if (sourceText == null) return undefined; + + return createSourceFile(fileName, sourceText, languageVersion, true, ScriptKind.TS); + }, + + getCurrentDirectory() { + return "."; + }, + + getDirectories(directoryName: string) { + return sys.getDirectories(directoryName); + }, + + getDefaultLibFileName(options: CompilerOptions): string { + return getDefaultLibFileName(options); + }, + + getCanonicalFileName(fileName: string): string { + return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); + }, + + getNewLine(): string { + return sys.newLine; + }, + + useCaseSensitiveFileNames() { + return sys.useCaseSensitiveFileNames; + } + } + }); +} diff --git a/packages/ts-simple-type/test/helpers/generate-assignment-markdown.ts b/packages/ts-simple-type/test/helpers/generate-assignment-markdown.ts new file mode 100644 index 00000000..24fbd72d --- /dev/null +++ b/packages/ts-simple-type/test/helpers/generate-assignment-markdown.ts @@ -0,0 +1,73 @@ +import { writeFileSync } from "fs"; +import { join } from "path"; +import { generateCombinedTypeTestCode } from "./generate-combined-type-test-code"; +import { markdownTable } from "./markdown-util"; +import type { TypescriptType } from "./type-test"; +import { visitComparisonsInTestCode } from "./visit-type-comparisons"; +import type { CompilerOptions } from "typescript"; +import { PRIMITIVE_TYPES, SPECIAL_TYPES } from "../type-combinations.spec"; + +/** + * Generates a markdown table that shows assignability between types. + * @param types + * @param compilerOptions + */ +export function generateAssignmentTableMarkdown(types: TypescriptType[], compilerOptions: CompilerOptions = {}) { + const typeResults = new Map>(); + + // Save all assignments to the nested map + const testCode = generateCombinedTypeTestCode(types, types); + visitComparisonsInTestCode(testCode, compilerOptions, ({ assignable, typeAString, typeBString }) => { + const typeResult = typeResults.get(typeAString) || new Map(); + typeResult.set(typeBString, assignable); + typeResults.set(typeAString, typeResult); + }); + + // Generate header row using the keys of "typeResult" (this is typeB). + // We can do this because all types are inserted into the map in the same order + const headerRow: string[] = ["typeB ➡️\ntypeA ⬇️", ...Array.from(typeResults.keys())]; + + // Generate all table rows using the nested maps + const rows: string[][] = Array.from(typeResults.entries()).map(([title, typeResult]) => [ + title, + ...Array.from(typeResult.values()).map(assignable => (assignable ? "✅" : "❌")) + ]); + const tableRows = [headerRow, ...rows]; + + return markdownTable(tableRows); +} + +/** + * Generates markdown showing assignability between types in different modes. + * @param types + * @param path + */ +export function generateAssignmentMarkdown(types: TypescriptType[], path: string = "./assignments.md") { + const strictTableMarkdown = generateAssignmentTableMarkdown(types, { strict: true }); + const nonStrictTableMarkdown = generateAssignmentTableMarkdown(types, { strict: false }); + + return `# Assignments +This table illustrates which types can be assigned to each other. + +Each cell shows if the assignment \`typeA = typeB\` is valid. + + ## Assignments with strict options: + ${strictTableMarkdown} + + ## Assignments with non-strict options: + ${nonStrictTableMarkdown} + `; +} + +/** + * Writes assignment comparison markdown to the file system. + * @param types + * @param path + */ +export function writeAssignmentMarkdown(path: string = "./assignments.md") { + const markdown = generateAssignmentMarkdown([...PRIMITIVE_TYPES, ...SPECIAL_TYPES, "{}", "void"]); + const absolutePath = join(process.cwd(), path); + // eslint-disable-next-line no-console + console.log(`Writing comparison table to ${absolutePath}`); + writeFileSync(absolutePath, markdown); +} diff --git a/packages/ts-simple-type/test/helpers/generate-combined-type-test-code.ts b/packages/ts-simple-type/test/helpers/generate-combined-type-test-code.ts new file mode 100644 index 00000000..2842dbb7 --- /dev/null +++ b/packages/ts-simple-type/test/helpers/generate-combined-type-test-code.ts @@ -0,0 +1,70 @@ +import type { TypescriptType, TypeTest } from "./type-test"; + +let randomId = 100; + +/** + * Prepares type test code for a type + * @param type + */ +function prepareTypeTestCode(type: TypescriptType): { setup?: string; type: string | string[] } { + if (typeof type === "string") { + return { type }; + } + + const id = randomId++; + + const setupCode = typeof type.setup === "string" ? type.setup : type.setup(id); + const typeCode = typeof type.type === "function" ? type.type(id) : type.type; + + return { + setup: setupCode, + type: typeCode + }; +} + +/** + * Generates code to test a specific combination of 2 types + * @param tA + * @param tB + */ +function generateTypeTestCode([tA, tB]: TypeTest): string[] { + const { type: typeA, setup: setupA } = prepareTypeTestCode(tA); + const { type: typeB, setup: setupB } = prepareTypeTestCode(tB); + + const testCode: string[] = []; + for (const tA of Array.isArray(typeA) ? typeA : [typeA]) { + for (const tB of Array.isArray(typeB) ? typeB : [typeB]) { + testCode.push( + [ + `{`, + ...(setupA != null ? [` ${setupA.replace(/\n/g, "\n ")}`] : []), + ...(setupB != null ? [` ${setupB.replace(/\n/g, "\n ")}`] : []), + ` const _: ${tA} = {} as ${tB}`, + `}` + ].join("\n") + ); + } + } + + return testCode; +} + +/** + * Generates code to test all combinations of the types + * @param typesX + * @param typesY + */ +export function generateCombinedTypeTestCode(typesX: TypescriptType[], typesY: TypescriptType[]): string { + const setupCodeSet = new Set(); + const testCodeSet = new Set(); + + for (const testTypeX of typesX) { + for (const testTypeY of typesY) { + const typeTestCombination = [testTypeX, testTypeY] as TypeTest; + const testCode = generateTypeTestCode(typeTestCombination); + testCode.forEach(c => testCodeSet.add(c)); + } + } + + return `${Array.from(setupCodeSet).join("\n")}${Array.from(testCodeSet).join("\n")}`; +} diff --git a/packages/ts-simple-type/test/helpers/markdown-util.ts b/packages/ts-simple-type/test/helpers/markdown-util.ts new file mode 100644 index 00000000..d918386f --- /dev/null +++ b/packages/ts-simple-type/test/helpers/markdown-util.ts @@ -0,0 +1,99 @@ +/** + * Highlights some text + * @param text + */ +export function markdownHighlight(text: string): string { + return `\`${text}\``; +} + +/** + * Returns a markdown header with a specific level + * @param level + * @param title + */ +export function markdownHeader(level: number, title: string): string { + return `${"#".repeat(level)} ${title}`; +} + +export interface MarkdownTableOptions { + removeEmptyColumns: boolean; + minCellWidth: number; + maxCellWidth: number; + cellPadding: number; +} + +const DEFAULT_MARKDOWN_TABLE_OPTIONS: MarkdownTableOptions = { + removeEmptyColumns: true, + minCellWidth: 3, + maxCellWidth: 50, + cellPadding: 1 +}; + +/** + * Returns a markdown table representation of the rows. + * Strips unused columns. + * @param rows + * @param options + */ +export function markdownTable(rows: string[][], options: Partial = {}): string { + // Constants for pretty printing the markdown tables + const MIN_CELL_WIDTH = options.minCellWidth || DEFAULT_MARKDOWN_TABLE_OPTIONS.minCellWidth; + const MAX_CELL_WIDTH = options.maxCellWidth || DEFAULT_MARKDOWN_TABLE_OPTIONS.maxCellWidth; + const CELL_PADDING = options.cellPadding || DEFAULT_MARKDOWN_TABLE_OPTIONS.cellPadding; + + // Count the number of columns + let columnCount = Math.max(...rows.map(r => r.length)); + + if (options.removeEmptyColumns) { + // Create a boolean array where each entry tells if a column is used or not (excluding the header) + const emptyColumns = Array(columnCount) + .fill(false) + .map((b, i) => i !== 0 && rows.slice(1).find(r => r[i] != null && r[i].length > 0) == null); + + // Remove unused columns if necessary + if (emptyColumns.includes(true)) { + // Filter out the unused columns in each row + rows = rows.map(row => row.filter((column, i) => !emptyColumns[i])); + + // Adjust the column count + columnCount = Math.max(...rows.map(r => r.length)); + } + } + + // Escape all cells in the markdown output + rows = rows.map(r => r.map(markdownEscapeTableCell)); + + // Create a boolean array where each entry corresponds to the preferred column width. + // This is done by taking the largest width of all cells in each column. + const columnWidths = Array(columnCount) + .fill(0) + .map((c, i) => Math.min(MAX_CELL_WIDTH, Math.max(MIN_CELL_WIDTH, ...rows.map(r => (r[i] || "").length)) + CELL_PADDING * 2)); + + // Build up the table + return ` +|${rows[0].map((r, i) => fillWidth(r, columnWidths[i], CELL_PADDING)).join("|")}| +|${columnWidths.map(c => "-".repeat(c)).join("|")}| +${rows + .slice(1) + .map(r => `|${r.map((r, i) => fillWidth(r, columnWidths[i], CELL_PADDING)).join("|")}|`) + .join("\n")} +`; +} + +/** + * Escape a text so it can be used in a markdown table + * @param text + */ +function markdownEscapeTableCell(text: string): string { + return text.replace(/\n/g, "
").replace(/\|/g, "\\|"); +} + +/** + * Creates padding around some text with a target width. + * @param text + * @param width + * @param paddingStart + */ +function fillWidth(text: string, width: number, paddingStart: number): string { + return " ".repeat(paddingStart) + text + " ".repeat(Math.max(1, width - text.length - paddingStart)); +} diff --git a/packages/ts-simple-type/test/helpers/test-assignment.ts b/packages/ts-simple-type/test/helpers/test-assignment.ts new file mode 100644 index 00000000..09a16adf --- /dev/null +++ b/packages/ts-simple-type/test/helpers/test-assignment.ts @@ -0,0 +1,149 @@ +/* eslint-disable no-console */ +import test from "ava"; +import { existsSync, writeFileSync } from "fs"; +import type { CompilerOptions, Node } from "typescript"; +import { isBlock } from "typescript"; +import { inspect } from "util"; +import { isAssignableToType } from "../../src/is-assignable/is-assignable-to-type"; +import { toSimpleType } from "../../src/transform/to-simple-type"; +import { generateCombinedTypeTestCode } from "./generate-combined-type-test-code"; +import type { TypescriptType } from "./type-test"; +import { visitComparisonsInTestCode } from "./visit-type-comparisons"; + +/** + * Tests all type combinations with different options + * @param typesX + * @param typesY + */ +export function testAssignments(typesX: TypescriptType[], typesY: TypescriptType[]) { + let reproCodeStrict = ""; + if (process.env.STRICT == null || process.env.STRICT === "true") { + testCombinedTypeAssignment(typesX, typesY, { strict: true }, repro => (reproCodeStrict += `${repro}\n\n`)); + } + + let reproCodeNonStrict = ""; + if (process.env.STRICT == null || process.env.STRICT === "false") { + testCombinedTypeAssignment(typesX, typesY, { strict: false }, repro => (reproCodeNonStrict += `${repro}\n\n`)); + } + + // Run this after all tests have finished + test.after.always(() => { + // Write repro to playground + if (existsSync("./playground")) { + if (reproCodeStrict.length > 0) { + writeFileSync("./playground/repro-strict.ts", `// Command: DEBUG= STRICT= FILE=repro-strict.ts npm run playground\n\n${reproCodeStrict}`); + } + if (reproCodeNonStrict.length > 0) { + writeFileSync( + "./playground/repro-non-strict.ts", + `// Command: DEBUG= STRICT=false FILE=repro-non-strict.ts npm run playground\n\n${reproCodeNonStrict}` + ); + } + } + }); +} + +/** + * Tests all type combinations + * @param typesX + * @param typesY + * @param compilerOptions + * @param reportError + */ +export function testCombinedTypeAssignment( + typesX: TypescriptType[], + typesY: TypescriptType[], + compilerOptions: CompilerOptions = {}, + reportError: (reproCode: string) => void = () => {} +) { + const testTitleSet = new Set(); + + const onlyLines = process.env.LINE == null ? undefined : process.env.LINE.split(",").map(Number); + + const testCode = generateCombinedTypeTestCode(typesX, typesY); + visitComparisonsInTestCode( + testCode, + compilerOptions, + ({ assignable: expectedResult, nodeA, checker, program, typeA, typeB, typeAString, typeBString, line }) => { + if (onlyLines != null && !onlyLines.includes(line)) { + return; + } + + const testTitle = `Assignment test [${line}]: "${typeAString} === ${typeBString}", Options: {${Object.entries(compilerOptions) + .map(([k, v]) => `${k}: ${v}`) + .join(", ")}}`; + if (testTitleSet.has(testTitle)) return; + testTitleSet.add(testTitle); + + test(testTitle, t => { + const simpleTypeALazy = toSimpleType(typeA, checker, { eager: false }); + const simpleTypeBLazy = toSimpleType(typeB, checker, { eager: false }); + const simpleTypeAEager = toSimpleType(typeA, checker, { eager: true }); + const simpleTypeBEager = toSimpleType(typeB, checker, { eager: true }); + + const actualResultLazy = isAssignableToType(simpleTypeALazy, simpleTypeBLazy, program); + const actualResultEager = isAssignableToType(simpleTypeAEager, simpleTypeBEager, program); + + if (actualResultEager !== actualResultLazy) { + t.log("Simple Type A", inspect(simpleTypeAEager, false, 5, true)); + t.log("Simple Type B", inspect(simpleTypeBEager, false, 5, true)); + + return t.fail( + `Mismatch between what isAssignableToType(...) returns for lazy type vs eager type. Eager: ${actualResultEager}. Lazy: ${actualResultLazy}. Expected result: ${expectedResult}` + ); + } + + const actualResult = actualResultLazy; + const simpleTypeA = simpleTypeAEager; + const simpleTypeB = simpleTypeBEager; + + if (actualResult === expectedResult && process.env.DEBUG === "true") { + console.log(""); + console.log("\x1b[4m%s\x1b[0m", testTitle); + console.log(`Expected: ${expectedResult}, Actual: ${actualResult}`); + console.log(""); + console.log("\x1b[1m%s\x1b[0m", "Simple Type A"); + console.log(inspect(simpleTypeA, false, 10, true)); + console.log(""); + console.log("\x1b[1m%s\x1b[0m", "Simple Type B"); + console.log(inspect(simpleTypeB, false, 10, true)); + } + + if (actualResult !== expectedResult) { + t.log("Simple Type A", inspect(simpleTypeA, false, 5, true)); + t.log("Simple Type B", inspect(simpleTypeB, false, 5, true)); + + const failText = `${actualResult ? "Can" : "Can't"} assign '${typeBString}' (${simpleTypeB.kind}) to '${typeAString}' (${simpleTypeA.kind}) but ${ + expectedResult ? "it should be possible!" : "it shouldn't be possible!" + }`; + + // Report repro code for the playground + const blockNode = findBlockNode(nodeA); + if (blockNode != null) { + // Generate debug log + let log = ""; + isAssignableToType(simpleTypeALazy, simpleTypeBLazy, program, { debug: true, debugLog: text => (log += `${text}\n`) }); + + reportError(`${log.length > 0 ? `/*\n${log}*/\n\n` : ""}// ${failText}\n${blockNode.getText()}`); + } + + return t.fail(failText); + } else { + t.pass(); + } + }); + } + ); +} + +function findBlockNode(node: Node): Node | undefined { + if (isBlock(node)) { + return node; + } + + if (node.parent == null) { + return undefined; + } + + return findBlockNode(node.parent); +} diff --git a/packages/ts-simple-type/test/helpers/type-test.ts b/packages/ts-simple-type/test/helpers/type-test.ts new file mode 100644 index 00000000..03934f5c --- /dev/null +++ b/packages/ts-simple-type/test/helpers/type-test.ts @@ -0,0 +1,8 @@ +export interface ITypescriptType { + setup: ((id: number) => string) | string; + type: ((id: number) => string | string[]) | string | string[]; +} + +export type TypescriptType = ITypescriptType | string; + +export type TypeTest = [TypescriptType, TypescriptType]; diff --git a/packages/ts-simple-type/test/helpers/visit-type-comparisons.ts b/packages/ts-simple-type/test/helpers/visit-type-comparisons.ts new file mode 100644 index 00000000..b17209a0 --- /dev/null +++ b/packages/ts-simple-type/test/helpers/visit-type-comparisons.ts @@ -0,0 +1,75 @@ +import type { CompilerOptions, Node, Program, Type, TypeChecker } from "typescript"; +import { isBinaryExpression, isVariableDeclaration, SyntaxKind } from "typescript"; +import { programWithVirtualFiles } from "./analyze-text"; +import { generateCombinedTypeTestCode } from "./generate-combined-type-test-code"; +import type { TypescriptType } from "./type-test"; + +/** + * Visits all type comparisons by traversing the AST recursively + * @param node + * @param foundAssignment + */ +export function visitNodeComparisons(node: Node, foundAssignment: (options: { line: number; nodeA: Node; nodeB: Node }) => void): void { + if (isVariableDeclaration(node) && node.initializer != null) { + const line = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line; + foundAssignment({ line, nodeA: node, nodeB: node.initializer }); + } else if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.EqualsToken) { + const line = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line; + foundAssignment({ line, nodeA: node.left, nodeB: node.right }); + } + + node.forEachChild(child => visitNodeComparisons(child, foundAssignment)); +} + +export interface VisitComparisonInTestCodeOptions { + line: number; + typeA: Type; + typeB: Type; + nodeA: Node; + nodeB: Node; + program: Program; + typeAString: string; + typeBString: string; + checker: TypeChecker; + assignable: boolean; +} + +/** + * Visits all type comparisons and emits them through the callback + * @param testCode + * @param callback + * @param compilerOptions + */ +export function visitComparisonsInTestCode( + testCode: string, + compilerOptions: CompilerOptions, + callback: (options: VisitComparisonInTestCodeOptions) => void +) { + const program = programWithVirtualFiles({ fileName: "test-code.ts", text: testCode }, { options: compilerOptions, includeLib: true }); + + const [sourceFile] = program.getSourceFiles().filter(f => f.fileName.includes("test-code")); + + const checker = program.getTypeChecker(); + + visitNodeComparisons(sourceFile, ({ line, nodeA, nodeB }) => { + const typeA = checker.getTypeAtLocation(nodeA); + const typeB = checker.getTypeAtLocation(nodeB); + + const typeAString = checker.typeToString(typeA); + const typeBString = checker.typeToString(typeB); + + const assignable = checker.isTypeAssignableTo(typeB, typeA); + + callback({ line, typeA, typeB, typeAString, typeBString, program, checker, assignable, nodeA, nodeB }); + }); +} + +export function visitTypeComparisons( + typesX: TypescriptType[], + typesY: TypescriptType[], + compilerOptions: CompilerOptions, + callback: (options: VisitComparisonInTestCodeOptions) => void +): void { + const testCode = generateCombinedTypeTestCode(typesX, typesY); + visitComparisonsInTestCode(testCode, compilerOptions, callback); +} diff --git a/packages/ts-simple-type/test/infinity.spec.ts b/packages/ts-simple-type/test/infinity.spec.ts new file mode 100644 index 00000000..88a9dba7 --- /dev/null +++ b/packages/ts-simple-type/test/infinity.spec.ts @@ -0,0 +1,38 @@ +import test from "ava"; +import * as ts from "typescript"; +import type { Program } from "typescript"; +import type { SimpleType } from "../src"; +import { deserializeSimpleType, serializeSimpleType } from "../src"; +import { isAssignableToSimpleType } from "../src/is-assignable/is-assignable-to-simple-type"; +import { toSimpleType } from "../src/transform/to-simple-type"; +import { programWithVirtualFiles } from "./helpers/analyze-text"; + +test("Large, recursive types that match on structure but not reference should not continue forever", t => { + const program = programWithVirtualFiles("", { includeLib: true }); + + const typeA = getLibTypeWithName("MouseEvent", program)!; + + // Serialize+deserialize the type in order to change the reference + const typeB = deserializeSimpleType(serializeSimpleType(getLibTypeWithName("MouseEvent", program)!)); + + const result = isAssignableToSimpleType(typeA, typeB); + + t.truthy(result); +}); + +export function getLibTypeWithName(name: string, program: Program): SimpleType | undefined { + for (const libFileName of ["lib.dom.d.ts"]) { + const sourceFile = program.getSourceFile(libFileName); + if (sourceFile == null) { + continue; + } + + for (const statement of sourceFile.statements) { + if (ts.isInterfaceDeclaration(statement) && statement.name?.text === name) { + return toSimpleType(statement, program.getTypeChecker()); + } + } + } + + return undefined; +} diff --git a/packages/ts-simple-type/test/type-combinations.spec.ts b/packages/ts-simple-type/test/type-combinations.spec.ts new file mode 100644 index 00000000..7830d7c9 --- /dev/null +++ b/packages/ts-simple-type/test/type-combinations.spec.ts @@ -0,0 +1,373 @@ +import { testAssignments } from "./helpers/test-assignment"; +import type { TypescriptType } from "./helpers/type-test"; + +export const BOOLEAN_TYPES: TypescriptType[] = [`Boolean`, `true`, `false`, `boolean`]; + +export const NUMBER_TYPES: TypescriptType[] = [`Number`, `123`, `number`, `42`]; + +export const STRING_TYPES: TypescriptType[] = [`String`, `"foo"`, `string`, `"bar"`]; + +export const BIG_INT_TYPES: TypescriptType[] = [`BigInt`, `123n`, `bigint`, `42n`]; + +export const ES_SYMBOL_TYPES: TypescriptType[] = [`Symbol`, `Symbol("hello")`, `unique symbol`, `symbol`]; + +export const NULLABLE_TYPES: TypescriptType[] = [`undefined`, `null`]; + +export const PRIMITIVE_TYPES: TypescriptType[] = [ + ...BOOLEAN_TYPES, + ...NUMBER_TYPES, + ...STRING_TYPES, + ...NULLABLE_TYPES, + ...BIG_INT_TYPES, + ...ES_SYMBOL_TYPES +]; + +export const SPECIAL_TYPES: TypescriptType[] = [`never`, `void`, `any`, `unknown`]; + +export const TUPLE_TYPES: TypescriptType[] = [ + `[]`, + `[string]`, + `[string, number]`, + `[string, boolean?]`, + `[string, ...boolean[]]`, + `[{ foo: string, bar: number }]` +]; + +export const ARRAY_TYPES: TypescriptType[] = [ + `Array`, + `string[]`, + `number[]` /*, `ReadonlyArray`*/, + `(number | string)[]`, + `["foo", 123]`, + `["foo", true, 123]`, + `{ foo: string, bar: number }[];` +]; + +export const CLASS_TYPES: TypescriptType[] = [ + { + setup: id => `class EmptyClass${id} {}`, + type: id => `EmptyClass${id}` + }, + { + setup: id => `class ClassWithOptional${id} { a?: number; }`, + type: id => `ClassWithOptional${id}` + }, + { + setup: id => ` +class CallableClassA${id} { + (call: number): string; + a: number; +}`, + type: id => `CallableClassA${id}` + }, + { + setup: id => ` +class CallableClassB${id} { + (call: string): CallableClassB; + a: number; +}`, + type: id => `CallableClassB${id}` + }, + { + setup: id => ` +class CtorClassA${id} { + constructor (input: string) { } +}`, + type: id => `CtorClassA${id}` + }, + { + setup: id => ` +class CtorClassB${id} { + constructor (input: number) { } +}`, + type: id => `CtorClassB${id}` + } +]; + +export const OBJECT_TYPES: TypescriptType[] = [ + `Object`, + `object`, + `{}`, + `{a: string}`, + `{a: string, b: number}`, + `{a: number}`, + `{foo: "", bar: true}`, + `{a?: number | string}`, + `{(): string}`, + `{(): string, a?: number}`, + `{(a: number): string, a: string | number}`, + `{ hello(t: string): number }`, + `{ new(input: number): any }`, + `{ new(input: string): any }`, + `{ new(): any, a: number }`, + `{ x : number|string, () : void }` +]; + +export const INTERFACE_TYPES: TypescriptType[] = [ + { + setup: id => `interface MyInterfaceA${id} {a: string}`, + type: id => `MyInterfaceA${id}` + }, + { + setup: id => `interface MyInterfaceB${id} {a: string, b: number}`, + type: id => `MyInterfaceB${id}` + }, + { + setup: id => `interface MyInterfaceC${id} {a: number}`, + type: id => `MyInterfaceC${id}` + }, + { + setup: id => `interface MyInterfaceD${id} {a?: number}`, + type: id => `MyInterfaceD${id}` + }, + { + setup: id => `interface MyInterfaceD${id} {(a: string): number}`, + type: id => `MyInterfaceD${id}` + }, + { + setup: id => `interface MyInterfaceE${id} {(a: string): number, a: string}`, + type: id => `MyInterfaceE${id}` + }, + { + setup: id => `interface MyInterfaceF${id} {(a: string): number, a: number}`, + type: id => `MyInterfaceF${id}` + }, + { + setup: id => `interface MyInterfaceG${id} {new (input: string): MyInterfaceG${id}`, + type: id => `MyInterfaceG${id}` + }, + { + setup: id => ` +interface InterfaceWithReadonlyA${id} { + readonly a?: boolean; + readonly b: number; +}`, + type: id => `InterfaceWithReadonlyA${id}` + }, + { + setup: id => ` +interface InterfaceWithReadonlyB${id} { + readonly b: number | string; +}`, + type: id => `InterfaceWithReadonlyB${id}` + } +]; + +export const FUNCTION_TYPES: TypescriptType[] = [ + //`Function`, + `(value: string) => void`, + `(() => void)`, + `((a: string) => void)`, + `(() => string)`, + `((a: number) => string)`, + `((a: number, b: number, c: number, d: number) => string)`, + `((a: number, b?: string) => string)`, + `((a: number, b: string) => string)`, + `(): string | null`, + `(): string | boolean | null | {a: string}` +]; + +export const FUNCTION_THIS_TYPES: TypescriptType[] = [ + `(this: string, a: number) => any`, + `(this: number, a: number) => any`, + `(this: number) => any` +]; + +export const FUNCTION_REST_TYPES: TypescriptType[] = [ + `(...spread: number[]) => boolean`, + `(...spread: (string | number)[]) => boolean`, + `(a: number, b?: string, ...args: number[]) => boolean` +]; + +export const UNION_TYPES: TypescriptType[] = [`string | number`, `undefined | null | string`]; + +export const INTERSECTION_TYPES: TypescriptType[] = [ + `{ foo: string }[] & { bar: number }[]`, + `{ foo: string, bar: boolean } & { hello (): void };`, + `[{ foo: string }] & { bar: number }`, + `[string, number] & [string, number]`, + `[string, number] & [string]`, + "1 & 2", + "'foo' & 'bar'", + "number & string" + /*{ // TODO: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#intersections-reduced-by-discriminant-properties + setup: ` +interface Circle { + kind: "circle"; + radius: number; +} + +interface Square { + kind: "square"; + sideLength: number; +} +`, + type: `Circle & Square` + }*/ +]; + +export const EXTRA_TYPES: TypescriptType[] = [`Date`, `Promise`, `Promise`, `PathLike`]; + +export const TYPE_ALIAS_TYPES: TypescriptType[] = [ + { + setup: id => `type MyTypeAlias${id} = String`, + type: id => `MyTypeAlias${id}` + }, + { + setup: id => `type MyTypeAlias${id} = string`, + type: id => `MyTypeAlias${id}` + }, + { + setup: id => `type MyTypeAlias1${id} = string; type MyTypeAlias2${id} = MyTypeAlias1${id}`, + type: id => `MyTypeAlias2${id}` + }, + { + setup: `type MyTypeAlias2 = T`, + type: `MyTypeAlias2` + } +]; + +export const ENUM_TYPES: TypescriptType[] = [ + { + setup: `enum Colors {RED = "RED", GREEN = "GREEN", BLUE = "BLUE"}`, + type: `Colors.GREEN` + }, + /*{ + setup: id => `enum UniqueColorEnum${id} {RED = "RED", GREEN = "GREEN", BLUE = "BLUE"}`, + type: id => `UniqueColorEnum${id}.GREEN` + },*/ + { + setup: `enum Sizes {SMALL, MEDIUM, LARGE}`, + type: `Sizes.MEDIUM` + } +]; + +export const CIRCULAR_REF_TYPES: TypescriptType[] = [ + `Element`, + `HTMLElement`, + `Event`, + `(typeof HTMLElement["prototype"]["addEventListener"])`, + `DocumentFragment`, + `NodeListOf`, + `EventTarget`, + `ChildNode`, + `HTMLSlotElement`, + `EventListenerOrEventListenerObject`, + `AssignedNodesOptions`, + { + setup: ` +interface B { next: C; } +interface C { next: D; } +interface D { next: B; }`, + type: "B" + }, + { + setup: ` +interface List { + data: T; + next: List; + owner: List>; +}`, + type: `List` + }, + { + setup: id => ` +interface MyCircularInterfaceA${id} { + foo: string; + bar: MyCircularInterfaceA${id}; +}`, + type: id => `MyCircularInterfaceB${id}` + }, + { + setup: id => ` +interface MyCircularInterfaceGeneric${id} { + hello(t: MyCircularInterfaceGeneric${id}): void; +} + `, + type: id => [`MyCircularInterfaceGeneric${id}`, `MyCircularInterfaceGeneric${id}`] + }, + { + setup: id => ` +export interface InterfaceWithRecursiveGenericB${id} {} + +export interface InterfaceWithRecursiveGenericC${id} { + data?: T; +} + +export interface InterfaceWithRecursiveGenericA${id} extends InterfaceWithRecursiveGenericB${id}> { + options: InterfaceWithRecursiveGenericC${id}; +}`, + type: id => [`InterfaceWithRecursiveGenericA${id}`, `InterfaceWithRecursiveGenericA${id}<"hello">`] + } +]; + +export const LIB_TYPES: TypescriptType[] = [`require("typescript").Node`, `require("fs").PathLike`]; + +export const GENERIC_TYPES: TypescriptType[] = [ + { + setup: id => ` +interface GenericInterfaceA${id} { + foo: T; + bar: U; +}`, + type: id => [`GenericInterfaceA${id}`, `GenericInterfaceA${id}`, `GenericInterfaceA${id}<"hello", 123>`] + }, + { + setup: id => ` +class GenericClassA${id} { + foo!: T; + + hello(t: T): U | R { + return {} as U; + } +}`, + type: id => [`GenericClassA${id}`, `GenericClassA${id}`] + }, + { + setup: id => `type GenericFunctionA${id} = (t: T) => T | undefined`, + type: id => [`GenericFunctionA${id}`, `GenericFunctionA${id}`] + }, + { + setup: id => `type GenericFunctionB${id} = (x: T, y: U) => [T, U]`, + type: id => [`GenericFunctionB${id}<"hello">`, `GenericFunctionB${id}`] + }, + { + setup: id => `type GenericFunctionC${id} = (x: S, y: S) => [S, S]`, + type: id => [`GenericFunctionC${id}`, `GenericFunctionC${id}`] + } + //`(value: T | PromiseLike) => void` + /*` +{ + foo: "hello"; + hello(t: string): U +}`,*/ +]; + +export const CUSTOM_TYPES: TypescriptType[] = [`Promise`, `Promise`, `Promise`]; + +export const ALL_TYPES: TypescriptType[] = [ + ...PRIMITIVE_TYPES, + ...TYPE_ALIAS_TYPES, + ...ENUM_TYPES, + ...SPECIAL_TYPES, + ...TUPLE_TYPES, + ...ARRAY_TYPES, + ...INTERFACE_TYPES, + ...OBJECT_TYPES, + ...CLASS_TYPES, + ...FUNCTION_TYPES, + ...FUNCTION_THIS_TYPES, + ...FUNCTION_REST_TYPES, + ...UNION_TYPES, + ...EXTRA_TYPES, + ...CIRCULAR_REF_TYPES, + ...LIB_TYPES, + ...GENERIC_TYPES, + ...CUSTOM_TYPES, + ...INTERSECTION_TYPES +]; + +const A_TYPES = process.env.TYPEA == null ? ALL_TYPES : process.env.TYPEA.split(";"); +const B_TYPES = process.env.TYPEB == null ? ALL_TYPES : process.env.TYPEB.split(";"); + +testAssignments(A_TYPES, B_TYPES); diff --git a/packages/ts-simple-type/tsconfig.json b/packages/ts-simple-type/tsconfig.json new file mode 100644 index 00000000..20d6bb8b --- /dev/null +++ b/packages/ts-simple-type/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/**/*", "./test/**/*", "./package.json", "./run-playground.ts", "./rollup.config.mjs"], + "compilerOptions": { + "tsBuildInfoFile": "./.tsbuildinfo" + } +} diff --git a/packages/ts-simple-type/tsconfig.prod.json b/packages/ts-simple-type/tsconfig.prod.json new file mode 100644 index 00000000..cbcf731a --- /dev/null +++ b/packages/ts-simple-type/tsconfig.prod.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "compilerOptions": { + "incremental": false, + "outDir": "./lib" + } +} diff --git a/packages/vscode-lit-plugin/package.json b/packages/vscode-lit-plugin/package.json index 4e580f12..16dce759 100644 --- a/packages/vscode-lit-plugin/package.json +++ b/packages/vscode-lit-plugin/package.json @@ -170,7 +170,6 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "^22.14.1", "@types/vscode": "^1.30.0", "@vscode/vsce": "^3.3.2", "esbuild": "^0.25.1", diff --git a/packages/web-component-analyzer/package.json b/packages/web-component-analyzer/package.json index 8efb4391..93e0f907 100644 --- a/packages/web-component-analyzer/package.json +++ b/packages/web-component-analyzer/package.json @@ -28,10 +28,15 @@ }, "wireit": { "build": { + "dependencies": [ + "../ts-simple-type:build", + "build:types" + ], "command": "rollup -c", "files": [ "src/**/*", "test/**/*", + "rollup.config.mjs", "tsconfig.json", "../../tsconfig.json" ], @@ -42,6 +47,9 @@ ], "clean": "if-file-deleted" }, + "build:types": { + "command": "tsc --build --pretty --noEmit" + }, "test": { "dependencies": [ "test:all" @@ -84,21 +92,20 @@ "img": "https://avatars0.githubusercontent.com/u/5372940?s=400&u=43d97899257af3c47715679512919eadb07eab26&v=4" } ], - "author": "Rune Mehlsen", + "author": "JackRobards", "license": "MIT", "bugs": { "url": "https://github.com/JackRobards/lit-analyzer/issues" }, "homepage": "https://github.com/JackRobards/lit-analyzer#readme", "dependencies": { - "ts-simple-type": "2.0.0-next.0", + "@jackolope/ts-simple-type": "^3.0.0", "typescript": "^5.8.3" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-typescript": "^12.1.2", - "@types/node": "^22.14.1", "ava": "^6.2.0", "cross-env": "^7.0.2", "rollup": "^4.39.0", diff --git a/packages/web-component-analyzer/rollup.config.mjs b/packages/web-component-analyzer/rollup.config.mjs index 35f89784..c0c82cc9 100644 --- a/packages/web-component-analyzer/rollup.config.mjs +++ b/packages/web-component-analyzer/rollup.config.mjs @@ -8,7 +8,7 @@ const require = createRequire(import.meta.url); const { dirname } = require("path"); const pkg = require("./package.json"); const watch = { include: "src/**" }; -const external = ["typescript", "path", "fs", "ts-simple-type"]; +const external = ["typescript", "path", "fs", "@jackolope/ts-simple-type"]; const replaceVersionConfig = { VERSION: pkg.version, delimiters: ["<@", "@>"], diff --git a/packages/web-component-analyzer/src/analyze/flavors/custom-element/discover-members.ts b/packages/web-component-analyzer/src/analyze/flavors/custom-element/discover-members.ts index ea065da7..fe75f0d1 100644 --- a/packages/web-component-analyzer/src/analyze/flavors/custom-element/discover-members.ts +++ b/packages/web-component-analyzer/src/analyze/flavors/custom-element/discover-members.ts @@ -1,4 +1,4 @@ -import { toSimpleType } from "ts-simple-type"; +import { toSimpleType } from "@jackolope/ts-simple-type"; import type { BinaryExpression, ExpressionStatement, Node, ReturnStatement } from "typescript"; import type { ComponentMember } from "../../types/features/component-member"; import { getMemberVisibilityFromNode, getModifiersFromNode, hasModifier } from "../../util/ast-util"; diff --git a/packages/web-component-analyzer/src/analyze/flavors/js-doc/discover-features.ts b/packages/web-component-analyzer/src/analyze/flavors/js-doc/discover-features.ts index 12890a12..8249fcde 100644 --- a/packages/web-component-analyzer/src/analyze/flavors/js-doc/discover-features.ts +++ b/packages/web-component-analyzer/src/analyze/flavors/js-doc/discover-features.ts @@ -1,4 +1,4 @@ -import type { SimpleTypeStringLiteral } from "ts-simple-type"; +import type { SimpleTypeStringLiteral } from "@jackolope/ts-simple-type"; import type { Node } from "typescript"; import type { AnalyzerVisitContext } from "../../analyzer-visit-context"; import type { ComponentCssPart } from "../../types/features/component-css-part"; diff --git a/packages/web-component-analyzer/src/analyze/flavors/lit-element/parse-lit-property-configuration.ts b/packages/web-component-analyzer/src/analyze/flavors/lit-element/parse-lit-property-configuration.ts index dd699912..5b02e8a6 100644 --- a/packages/web-component-analyzer/src/analyze/flavors/lit-element/parse-lit-property-configuration.ts +++ b/packages/web-component-analyzer/src/analyze/flavors/lit-element/parse-lit-property-configuration.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type * as tsModule from "typescript"; import type { CallExpression, Node, PropertyAssignment } from "typescript"; import type { AnalyzerVisitContext } from "../../analyzer-visit-context"; diff --git a/packages/web-component-analyzer/src/analyze/types/features/component-event.ts b/packages/web-component-analyzer/src/analyze/types/features/component-event.ts index 5ac19480..01e35855 100644 --- a/packages/web-component-analyzer/src/analyze/types/features/component-event.ts +++ b/packages/web-component-analyzer/src/analyze/types/features/component-event.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { Node, Type } from "typescript"; import type { VisibilityKind } from "../visibility-kind"; import type { ComponentFeatureBase } from "./component-feature"; diff --git a/packages/web-component-analyzer/src/analyze/types/features/component-member.ts b/packages/web-component-analyzer/src/analyze/types/features/component-member.ts index 8c9dcf80..743e7e8a 100644 --- a/packages/web-component-analyzer/src/analyze/types/features/component-member.ts +++ b/packages/web-component-analyzer/src/analyze/types/features/component-member.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { Node, Type } from "typescript"; import type { PriorityKind } from "../../flavors/analyzer-flavor"; import type { ModifierKind } from "../modifier-kind"; diff --git a/packages/web-component-analyzer/src/analyze/types/features/component-method.ts b/packages/web-component-analyzer/src/analyze/types/features/component-method.ts index 434b6785..5b967661 100644 --- a/packages/web-component-analyzer/src/analyze/types/features/component-method.ts +++ b/packages/web-component-analyzer/src/analyze/types/features/component-method.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { Node, Type } from "typescript"; import type { VisibilityKind } from "../visibility-kind"; import type { ComponentFeatureBase } from "./component-feature"; diff --git a/packages/web-component-analyzer/src/analyze/types/features/lit-element-property-config.ts b/packages/web-component-analyzer/src/analyze/types/features/lit-element-property-config.ts index 588dbd27..d66df43f 100644 --- a/packages/web-component-analyzer/src/analyze/types/features/lit-element-property-config.ts +++ b/packages/web-component-analyzer/src/analyze/types/features/lit-element-property-config.ts @@ -1,4 +1,4 @@ -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import type { CallExpression, Node } from "typescript"; export interface LitElementPropertyConfig { diff --git a/packages/web-component-analyzer/src/analyze/util/ast-util.ts b/packages/web-component-analyzer/src/analyze/util/ast-util.ts index 9a934a7e..da7db822 100644 --- a/packages/web-component-analyzer/src/analyze/util/ast-util.ts +++ b/packages/web-component-analyzer/src/analyze/util/ast-util.ts @@ -1,4 +1,4 @@ -import { isAssignableToSimpleTypeKind } from "ts-simple-type"; +import { isAssignableToSimpleTypeKind } from "@jackolope/ts-simple-type"; import type * as tsModule from "typescript"; import type { Decorator, diff --git a/packages/web-component-analyzer/src/analyze/util/js-doc-util.ts b/packages/web-component-analyzer/src/analyze/util/js-doc-util.ts index 09e772d3..4ef489ff 100644 --- a/packages/web-component-analyzer/src/analyze/util/js-doc-util.ts +++ b/packages/web-component-analyzer/src/analyze/util/js-doc-util.ts @@ -1,4 +1,4 @@ -import type { SimpleType, SimpleTypeStringLiteral } from "ts-simple-type"; +import type { SimpleType, SimpleTypeStringLiteral } from "@jackolope/ts-simple-type"; import type * as tsModule from "typescript"; import type { JSDoc, JSDocParameterTag, JSDocTypeTag, Node, Program } from "typescript"; import { arrayDefined } from "../../util/array-util"; diff --git a/packages/web-component-analyzer/src/analyze/util/type-util.ts b/packages/web-component-analyzer/src/analyze/util/type-util.ts index a6138359..cf696389 100644 --- a/packages/web-component-analyzer/src/analyze/util/type-util.ts +++ b/packages/web-component-analyzer/src/analyze/util/type-util.ts @@ -1,5 +1,5 @@ -import type { SimpleType, SimpleTypeEnumMember } from "ts-simple-type"; -import { toSimpleType } from "ts-simple-type"; +import type { SimpleType, SimpleTypeEnumMember } from "@jackolope/ts-simple-type"; +import { toSimpleType } from "@jackolope/ts-simple-type"; import type * as tsModule from "typescript"; import type { Node, Program } from "typescript"; diff --git a/packages/web-component-analyzer/src/util/get-type-hint-from-type.ts b/packages/web-component-analyzer/src/util/get-type-hint-from-type.ts index 837c9332..74ca2a6a 100644 --- a/packages/web-component-analyzer/src/util/get-type-hint-from-type.ts +++ b/packages/web-component-analyzer/src/util/get-type-hint-from-type.ts @@ -1,5 +1,5 @@ -import type { SimpleType } from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import type { Type, TypeChecker } from "typescript"; /** diff --git a/packages/web-component-analyzer/src/util/strip-typescript-values.ts b/packages/web-component-analyzer/src/util/strip-typescript-values.ts index 9d874d53..bf1a4d57 100644 --- a/packages/web-component-analyzer/src/util/strip-typescript-values.ts +++ b/packages/web-component-analyzer/src/util/strip-typescript-values.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { isSimpleType, typeToString } from "ts-simple-type"; +import { isSimpleType, typeToString } from "@jackolope/ts-simple-type"; import type { Node, SourceFile, Type, TypeChecker } from "typescript"; function isTypescriptNode(value: any): value is Node { diff --git a/packages/web-component-analyzer/test/flavors/custom-element/discover-test.ts b/packages/web-component-analyzer/test/flavors/custom-element/discover-test.ts index ee55cddc..e7d8ae46 100644 --- a/packages/web-component-analyzer/test/flavors/custom-element/discover-test.ts +++ b/packages/web-component-analyzer/test/flavors/custom-element/discover-test.ts @@ -159,8 +159,14 @@ tsTest("Correctly discovers multiple declarations", t => { t.is(componentDefinitions.length, 1); t.is(componentDefinitions[0].tagName, "my-element"); - t.is(componentDefinitions[0].declaration?.members?.some(m => m.propName === "prototype"), false); - t.is(componentDefinitions[0].declaration?.methods?.some(m => m.name === "new"), false); + t.is( + componentDefinitions[0].declaration?.members?.some(m => m.propName === "prototype"), + false + ); + t.is( + componentDefinitions[0].declaration?.methods?.some(m => m.name === "new"), + false + ); }); tsTest("Discovers elements using typescript >=4.3 syntax", t => { diff --git a/packages/web-component-analyzer/test/flavors/custom-element/event-test.ts b/packages/web-component-analyzer/test/flavors/custom-element/event-test.ts index ee95046c..9c0b7a90 100644 --- a/packages/web-component-analyzer/test/flavors/custom-element/event-test.ts +++ b/packages/web-component-analyzer/test/flavors/custom-element/event-test.ts @@ -1,5 +1,5 @@ -import type { SimpleType} from "ts-simple-type"; -import { typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { typeToString } from "@jackolope/ts-simple-type"; import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module"; import { tsTest } from "../../helpers/ts-test"; diff --git a/packages/web-component-analyzer/test/flavors/jsdoc/event-test.ts b/packages/web-component-analyzer/test/flavors/jsdoc/event-test.ts index bc355871..a805f435 100644 --- a/packages/web-component-analyzer/test/flavors/jsdoc/event-test.ts +++ b/packages/web-component-analyzer/test/flavors/jsdoc/event-test.ts @@ -1,5 +1,5 @@ -import type { SimpleType} from "ts-simple-type"; -import { isAssignableToSimpleTypeKind, isAssignableToType, typeToString } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; +import { isAssignableToSimpleTypeKind, isAssignableToType, typeToString } from "@jackolope/ts-simple-type"; import { getLibTypeWithName } from "../../../src/analyze/util/type-util"; import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module"; import { getCurrentTsModule, tsTest } from "../../helpers/ts-test"; diff --git a/packages/web-component-analyzer/test/flavors/jsdoc/jsdoc-types.ts b/packages/web-component-analyzer/test/flavors/jsdoc/jsdoc-types.ts index 16fc3517..af3b9d60 100644 --- a/packages/web-component-analyzer/test/flavors/jsdoc/jsdoc-types.ts +++ b/packages/web-component-analyzer/test/flavors/jsdoc/jsdoc-types.ts @@ -1,5 +1,5 @@ import test from "ava"; -import type { SimpleType } from "ts-simple-type"; +import type { SimpleType } from "@jackolope/ts-simple-type"; import { parseSimpleJsDocTypeExpression } from "../../../src/analyze/util/js-doc-util"; import { getCurrentTsModule } from "../../helpers/ts-test"; diff --git a/packages/web-component-analyzer/test/flavors/polymer/polymer-test.ts b/packages/web-component-analyzer/test/flavors/polymer/polymer-test.ts index 24084151..b89da5fe 100644 --- a/packages/web-component-analyzer/test/flavors/polymer/polymer-test.ts +++ b/packages/web-component-analyzer/test/flavors/polymer/polymer-test.ts @@ -1,4 +1,4 @@ -import { isAssignableToSimpleTypeKind } from "ts-simple-type"; +import { isAssignableToSimpleTypeKind } from "@jackolope/ts-simple-type"; import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module"; import { tsTest } from "../../helpers/ts-test"; import { getComponentProp } from "../../helpers/util"; diff --git a/packages/web-component-analyzer/test/helpers/ts-test.ts b/packages/web-component-analyzer/test/helpers/ts-test.ts index 792aeec4..ea13b66a 100644 --- a/packages/web-component-analyzer/test/helpers/ts-test.ts +++ b/packages/web-component-analyzer/test/helpers/ts-test.ts @@ -7,7 +7,7 @@ type TestFunction = (title: string, implementation: ImplementationFn) const TS_MODULES_ALL = ["current", "5.4", "5.5", "5.6", "5.7"] as const; -type TsModuleKind = typeof TS_MODULES_ALL[number]; +type TsModuleKind = (typeof TS_MODULES_ALL)[number]; const TS_MODULES_DEFAULT: TsModuleKind[] = ["current", "5.4", "5.5", "5.6", "5.7"]; @@ -114,7 +114,11 @@ function setupTest(testFunction: TestFunction, tsModuleKind: TsModuleKind | unde * @param title * @param cb */ -function setupTests(testFunction: (title: string, implementation: ImplementationFn) => void, title: string, cb: ImplementationFn) { +function setupTests( + testFunction: (title: string, implementation: ImplementationFn) => void, + title: string, + cb: ImplementationFn +) { // Find the user specified TS_MODULE at setup time const moduleKinds: readonly TsModuleKind[] = (() => { const currentTsModuleKind = getCurrentTsModuleKind(); diff --git a/packages/web-component-analyzer/test/helpers/util.ts b/packages/web-component-analyzer/test/helpers/util.ts index e2bfbbd0..f7d5dff5 100644 --- a/packages/web-component-analyzer/test/helpers/util.ts +++ b/packages/web-component-analyzer/test/helpers/util.ts @@ -1,5 +1,5 @@ import type { ExecutionContext } from "ava"; -import { isAssignableToType, typeToString } from "ts-simple-type"; +import { isAssignableToType, typeToString } from "@jackolope/ts-simple-type"; import type { TypeChecker } from "typescript"; import type { ComponentMember, ComponentMemberProperty } from "../../src/analyze/types/features/component-member"; import { arrayDefined } from "../../src/util/array-util"; diff --git a/packages/web-component-analyzer/tsconfig.json b/packages/web-component-analyzer/tsconfig.json index c49644b6..8db6d043 100644 --- a/packages/web-component-analyzer/tsconfig.json +++ b/packages/web-component-analyzer/tsconfig.json @@ -5,6 +5,7 @@ "importHelpers": false, "lib": ["es2023", "dom"], "outDir": "./lib/esm", - "pretty": true + "pretty": true, + "tsBuildInfoFile": "./.tsbuildinfo" } }