diff --git a/deno/lib/__tests__/languageServerFeatures.source.ts b/deno/lib/__tests__/languageServerFeatures.source.ts new file mode 100644 index 000000000..c03a900ae --- /dev/null +++ b/deno/lib/__tests__/languageServerFeatures.source.ts @@ -0,0 +1,52 @@ +import * as z from "../index.ts"; + +export const Test = z.object({ + f1: z.number(), + f2: z.string().optional(), + f3: z.string().nullable(), + f4: z.array(z.object({ t: z.union([z.string(), z.boolean()]) })), +}); + +export type Test = z.infer; + +export const instanceOfTest: Test = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], +}; + +export const TestMerged = z + .object({ + f5: z.literal("literal").optional(), + }) + .merge(Test); + +export type TestMerged = z.infer; + +export const instanceOfTestMerged: TestMerged = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], + f5: "literal", +}; + +export const TestUnioned = z.union([ + z.object({ + f5: z.literal("literal").optional(), + }), + Test, +]); + +export type TestUnioned = z.infer; + +export const instanceOfTestUnioned: TestUnioned = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], + f5: "literal", +}; + +export const filePath = __filename; diff --git a/deno/lib/__tests__/languageServerFeatures.test.ts b/deno/lib/__tests__/languageServerFeatures.test.ts new file mode 100644 index 000000000..8f1e892bd --- /dev/null +++ b/deno/lib/__tests__/languageServerFeatures.test.ts @@ -0,0 +1,100 @@ +// @ts-ignore TS6133 +import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts"; +const test = Deno.test; +import { filePath } from "./languageServerFeatures.source.ts"; +import { Project, Node, SyntaxKind } from "ts-morph"; +import path from "path"; + +// The following tool is helpful for understanding the TypeScript AST associated with these tests: +// https://ts-ast-viewer.com/ (just copy the contents of languageServerFeatures.source into the viewer) + +describe("Executing Go To Definition (and therefore Find Usages and Rename Refactoring) using an IDE works on inferred object properties", () => { + // Compile file developmentEnvironment.source + const project = new Project({ + tsConfigFilePath: path.join(__dirname, "..", "..", "tsconfig.json"), + skipAddingFilesFromTsConfig: true, + }); + const sourceFile = project.addSourceFileAtPath(filePath); + + test("works for simple objects", () => { + // Find usage of Test.f1 property + const instanceVariable = + sourceFile.getVariableDeclarationOrThrow("instanceOfTest"); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of Test.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); + + test("works for merged objects", () => { + // Find usage of TestMerged.f1 property + const instanceVariable = sourceFile.getVariableDeclarationOrThrow( + "instanceOfTestMerged" + ); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of TestMerged.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); + + test("works for unioned objects", () => { + // Find usage of TestUnioned.f1 property + const instanceVariable = sourceFile.getVariableDeclarationOrThrow( + "instanceOfTestUnioned" + ); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of TestUnioned.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); +}); + +const getPropertyBeingAssigned = (node: Node, name: string) => { + const propertyAssignment = node.forEachDescendant((descendent) => + Node.isPropertyAssignment(descendent) && descendent.getName() == name + ? descendent + : undefined + ); + + if (propertyAssignment == null) + fail(`Could not find property assignment with name ${name}`); + + const propertyLiteral = propertyAssignment.getFirstDescendantByKind( + SyntaxKind.Identifier + ); + + if (propertyLiteral == null) + fail(`Could not find property literal with name ${name}`); + + return propertyLiteral; +}; diff --git a/package.json b/package.json index e48cdc607..bd53b26e8 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "pretty-quick": "^3.1.3", "rollup": "^2.70.1", "ts-jest": "^27.1.3", + "ts-morph": "^14.0.0", "ts-node": "^10.7.0", "tslib": "^2.3.1", "typescript": "^4.6.2" diff --git a/src/__tests__/languageServerFeatures.source.ts b/src/__tests__/languageServerFeatures.source.ts new file mode 100644 index 000000000..de9ed489b --- /dev/null +++ b/src/__tests__/languageServerFeatures.source.ts @@ -0,0 +1,52 @@ +import * as z from "../index"; + +export const Test = z.object({ + f1: z.number(), + f2: z.string().optional(), + f3: z.string().nullable(), + f4: z.array(z.object({ t: z.union([z.string(), z.boolean()]) })), +}); + +export type Test = z.infer; + +export const instanceOfTest: Test = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], +}; + +export const TestMerged = z + .object({ + f5: z.literal("literal").optional(), + }) + .merge(Test); + +export type TestMerged = z.infer; + +export const instanceOfTestMerged: TestMerged = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], + f5: "literal", +}; + +export const TestUnioned = z.union([ + z.object({ + f5: z.literal("literal").optional(), + }), + Test, +]); + +export type TestUnioned = z.infer; + +export const instanceOfTestUnioned: TestUnioned = { + f1: 1, + f2: "f2", + f3: "f3", + f4: [{ t: "f4" }, { t: true }], + f5: "literal", +}; + +export const filePath = __filename; diff --git a/src/__tests__/languageServerFeatures.test.ts b/src/__tests__/languageServerFeatures.test.ts new file mode 100644 index 000000000..e532bd549 --- /dev/null +++ b/src/__tests__/languageServerFeatures.test.ts @@ -0,0 +1,99 @@ +// @ts-ignore TS6133 +import { expect, fit } from "@jest/globals"; +import { filePath } from "./languageServerFeatures.source"; +import { Project, Node, SyntaxKind } from "ts-morph"; +import path from "path"; + +// The following tool is helpful for understanding the TypeScript AST associated with these tests: +// https://ts-ast-viewer.com/ (just copy the contents of languageServerFeatures.source into the viewer) + +describe("Executing Go To Definition (and therefore Find Usages and Rename Refactoring) using an IDE works on inferred object properties", () => { + // Compile file developmentEnvironment.source + const project = new Project({ + tsConfigFilePath: path.join(__dirname, "..", "..", "tsconfig.json"), + skipAddingFilesFromTsConfig: true, + }); + const sourceFile = project.addSourceFileAtPath(filePath); + + test("works for simple objects", () => { + // Find usage of Test.f1 property + const instanceVariable = + sourceFile.getVariableDeclarationOrThrow("instanceOfTest"); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of Test.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); + + test("works for merged objects", () => { + // Find usage of TestMerged.f1 property + const instanceVariable = sourceFile.getVariableDeclarationOrThrow( + "instanceOfTestMerged" + ); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of TestMerged.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); + + test("works for unioned objects", () => { + // Find usage of TestUnioned.f1 property + const instanceVariable = sourceFile.getVariableDeclarationOrThrow( + "instanceOfTestUnioned" + ); + const propertyBeingAssigned = getPropertyBeingAssigned( + instanceVariable, + "f1" + ); + + // Find definition of TestUnioned.f1 property + const definitionOfProperty = propertyBeingAssigned?.getDefinitionNodes()[0]; + const parentOfProperty = definitionOfProperty?.getFirstAncestorByKind( + SyntaxKind.VariableDeclaration + ); + + // Assert that find definition returned the Zod definition of Test + expect(definitionOfProperty?.getText()).toEqual("f1: z.number()"); + expect(parentOfProperty?.getName()).toEqual("Test"); + }); +}); + +const getPropertyBeingAssigned = (node: Node, name: string) => { + const propertyAssignment = node.forEachDescendant((descendent) => + Node.isPropertyAssignment(descendent) && descendent.getName() == name + ? descendent + : undefined + ); + + if (propertyAssignment == null) + fail(`Could not find property assignment with name ${name}`); + + const propertyLiteral = propertyAssignment.getFirstDescendantByKind( + SyntaxKind.Identifier + ); + + if (propertyLiteral == null) + fail(`Could not find property literal with name ${name}`); + + return propertyLiteral; +}; diff --git a/yarn.lock b/yarn.lock index fe52ffe54..b544138e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -828,6 +828,16 @@ resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@ts-morph/common@~0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.13.0.tgz#77dea1565baaf002d1bc2c20e05d1fb3349008a9" + integrity sha512-fEJ6j7Cu8yiWjA4UmybOBH9Efgb/64ZTWuvCF4KysGu4xz8ettfyaqFt8WZ1btCxXsGZJjZ2/3svOF6rL+UFdQ== + dependencies: + fast-glob "^3.2.11" + minimatch "^5.0.1" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -1394,6 +1404,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -1609,6 +1626,13 @@ co@^4.6.0: resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-block-writer@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.0.tgz#5956fb186617f6740e2c3257757fea79315dd7d4" + integrity sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w== + dependencies: + tslib "2.3.1" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" @@ -2267,7 +2291,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -3664,11 +3688,23 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mri@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -3924,6 +3960,11 @@ parse5@6.0.1: resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" @@ -4633,6 +4674,14 @@ ts-jest@^27.1.3: semver "7.x" yargs-parser "20.x" +ts-morph@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-14.0.0.tgz#6bffb7e4584cf6a9aebce2066bf4258e1d03f9fa" + integrity sha512-tO8YQ1dP41fw8GVmeQAdNsD8roZi1JMqB7YwZrqU856DvmG5/710e41q2XauzTYrygH9XmMryaFeLo+kdCziyA== + dependencies: + "@ts-morph/common" "~0.13.0" + code-block-writer "^11.0.0" + ts-node@^10.7.0: version "10.7.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" @@ -4681,16 +4730,16 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" +tslib@2.3.1, tslib@^2.1.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"