From 944d48dde145d1592152f0c0f4e81c4364c8ab2d Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sun, 10 Mar 2024 09:55:54 -0400 Subject: [PATCH] Support "equivalent to" for values (#252) Fixes #167 Weirdly the `Resolve` helper only resolves types at the top level, not in functions: ```ts interface Point { x: number; y: number; } type Resolve = Raw extends Function ? Raw : { [K in keyof Raw]: Raw[K] }; function foo(pt: Point) { let k: keyof Point; for (k in pt) { // ^? let k: keyof Point type SynthK = Resolve; // ^? type SynthK = keyof Point } } type SynthT2 = Resolve; // ^? type SynthT2 = "x" | "y"`); ``` This might be https://github.com/microsoft/TypeScript/issues/49852 --- src/code-sample.ts | 32 +++-- src/test/__snapshots__/asciidoc.test.ts.snap | 118 ++++++++++++++++++- src/test/code-sample.test.ts | 35 ++++++ src/test/inputs/equivalent.asciidoc | 24 ++++ 4 files changed, 197 insertions(+), 12 deletions(-) diff --git a/src/code-sample.ts b/src/code-sample.ts index f09b19c..54e2984 100644 --- a/src/code-sample.ts +++ b/src/code-sample.ts @@ -300,6 +300,13 @@ const EQUIVALENT_RE = /\^\? type ([A-Za-z0-9_]+) = (.*)( \(equivalent to (.*)\)) const EQUIVALENT_MULTILINE_RE = /\^\? type ([A-Za-z0-9_]+) = (.*)(\n\s*\/\/ +\(equivalent to (.*)\))$/m; +const VALUE_EQUIVALENT_RE = /\^\? [^ ]+ ([A-Za-z0-9_]+): (.*)( \(equivalent to (.*)\))$/m; +const VALUE_EQUIVALENT_MULTILINE_RE = + /\^\? [^ ]+ ([A-Za-z0-9_]+): (.*)(\n\s*\/\/ +\(equivalent to (.*)\))$/m; + +const RESOLVE_HELPER = + '\ntype Resolve = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};'; + /** Patch the code sample to test "equivalent to" types */ export function addResolvedChecks(sample: CodeSample): CodeSample { const {content} = sample; @@ -307,20 +314,31 @@ export function addResolvedChecks(sample: CodeSample): CodeSample { return sample; } + let synthName, type, equivClause, equivType; const m = EQUIVALENT_RE.exec(content) || EQUIVALENT_MULTILINE_RE.exec(content); - if (!m) { - return sample; + if (m) { + [, type, , equivClause, equivType] = m; + synthName = `Synth${type}`; + } else { + const mv = VALUE_EQUIVALENT_RE.exec(content) || VALUE_EQUIVALENT_MULTILINE_RE.exec(content); + if (mv) { + let varName; + [, varName, type, equivClause, equivType] = mv; + synthName = 'Synth' + varName.charAt(0).toUpperCase() + varName.slice(1); + } else { + return sample; + } } - const [, typeName, _raw, equivClause, equivType] = m; - // Strip the "equivalent to" bit, add Resolve helper and secondary type assertion. // See https://github.com/danvk/literate-ts/issues/132 and // https://effectivetypescript.com/2022/02/25/gentips-4-display/ + // Resolve is only able to "resolve" types at the top level; it can't be inserted in-place. + // So `type` could refer to something out of scope, but hopefully that doesn't happen. let newContent = content.replace(equivClause, ''); - newContent += '\ntype Resolve = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};'; - newContent += `\ntype Synth${typeName} = Resolve<${typeName}>;`; - newContent += `\n// ^? type Synth${typeName} = ${equivType}\n`; + newContent += RESOLVE_HELPER; + newContent += `\ntype ${synthName} = Resolve<${type}>;`; + newContent += `\n// ^? type ${synthName} = ${equivType}\n`; return { ...sample, diff --git a/src/test/__snapshots__/asciidoc.test.ts.snap b/src/test/__snapshots__/asciidoc.test.ts.snap index e41b7a9..b2c224c 100644 --- a/src/test/__snapshots__/asciidoc.test.ts.snap +++ b/src/test/__snapshots__/asciidoc.test.ts.snap @@ -105,7 +105,7 @@ exports[`checker asciidoc checker snapshots "./src/test/inputs/equivalent.asciid "logs": [ "---- BEGIN FILE ./src/test/inputs/equivalent.asciidoc ", - "Found 4 code samples in ./src/test/inputs/equivalent.asciidoc", + "Found 6 code samples in ./src/test/inputs/equivalent.asciidoc", "BEGIN #././src/test/inputs/equivalent.asciidoc:8 (--filter equivalent-8) ", "Code passed type checker.", @@ -191,16 +191,68 @@ type SynthT2 = Resolve; "tsconfig options: {"strictNullChecks":true,"module":1,"esModuleInterop":true}", " END #././src/test/inputs/equivalent.asciidoc:39 (--- ms) +", + "BEGIN #././src/test/inputs/equivalent.asciidoc:48 (--filter equivalent-48) +", + "Code passed type checker.", + "Twoslash type assertion match:", + " Expected: type T = keyof Point", + " Actual: type T = keyof Point", + "Twoslash type assertion match:", + " Expected: let k: keyof Point", + " Actual: let k: keyof Point", + "Twoslash type assertion match:", + " Expected: type SynthK = "x" | "y"", + " Actual: type SynthK = "x" | "y"", + " 3/3 twoslash type assertions matched.", + " +END #././src/test/inputs/equivalent.asciidoc:48 (--- ms) +", + "BEGIN #././src/test/inputs/equivalent.asciidoc:60 (--filter equivalent-60) +", + "Code passed type checker.", + "Twoslash type assertion match:", + " Expected: type T = keyof Point", + " Actual: type T = keyof Point", + "Twoslash type assertion match:", + " Expected: let k: keyof Point", + " Actual: let k: keyof Point", + "💥 ./src/test/inputs/equivalent.asciidoc:67:6-12: Failed type assertion for \`SynthK\` + Expected: type SynthK = "x" | "y" | "z" + Actual: type SynthK = "x" | "y"", + " 2/3 twoslash type assertions matched.", + "interface Point { + x: number; + y: number; +} + +type T = keyof Point; +// ^? type T = keyof Point +function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point + } +} +type Resolve = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]}; +type SynthK = Resolve; +// ^? type SynthK = "x" | "y" | "z" +", + "tsconfig options: {"strictNullChecks":true,"module":1,"esModuleInterop":true}", + " +END #././src/test/inputs/equivalent.asciidoc:60 (--- ms) ", "---- END FILE ./src/test/inputs/equivalent.asciidoc ", ], "statuses": [ "1/1: ././src/test/inputs/equivalent.asciidoc", - "1/1: ././src/test/inputs/equivalent.asciidoc: 1/4 ././src/test/inputs/equivalent.asciidoc:8", - "1/1: ././src/test/inputs/equivalent.asciidoc: 2/4 ././src/test/inputs/equivalent.asciidoc:23", - "1/1: ././src/test/inputs/equivalent.asciidoc: 3/4 ././src/test/inputs/equivalent.asciidoc:31", - "1/1: ././src/test/inputs/equivalent.asciidoc: 4/4 ././src/test/inputs/equivalent.asciidoc:39", + "1/1: ././src/test/inputs/equivalent.asciidoc: 1/6 ././src/test/inputs/equivalent.asciidoc:8", + "1/1: ././src/test/inputs/equivalent.asciidoc: 2/6 ././src/test/inputs/equivalent.asciidoc:23", + "1/1: ././src/test/inputs/equivalent.asciidoc: 3/6 ././src/test/inputs/equivalent.asciidoc:31", + "1/1: ././src/test/inputs/equivalent.asciidoc: 4/6 ././src/test/inputs/equivalent.asciidoc:39", + "1/1: ././src/test/inputs/equivalent.asciidoc: 5/6 ././src/test/inputs/equivalent.asciidoc:48", + "1/1: ././src/test/inputs/equivalent.asciidoc: 6/6 ././src/test/inputs/equivalent.asciidoc:60", ], } `; @@ -1013,6 +1065,62 @@ type T = keyof Point; "targetFilename": null, "tsOptions": {}, }, + { + "auxiliaryFiles": [], + "checkJS": false, + "content": "function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point (equivalent to "x" | "y") + } +}", + "descriptor": "./equivalent.asciidoc:48", + "id": "equivalent-48", + "isTSX": false, + "language": "ts", + "lineNumber": 47, + "nodeModules": [], + "prefixes": [ + { + "id": "equivalent-8", + }, + ], + "prefixesLength": 0, + "replacementId": undefined, + "sectionHeader": null, + "skip": false, + "sourceFile": "equivalent.asciidoc", + "targetFilename": null, + "tsOptions": {}, + }, + { + "auxiliaryFiles": [], + "checkJS": false, + "content": "function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point (equivalent to "x" | "y" | "z") + } +}", + "descriptor": "./equivalent.asciidoc:60", + "id": "equivalent-60", + "isTSX": false, + "language": "ts", + "lineNumber": 59, + "nodeModules": [], + "prefixes": [ + { + "id": "equivalent-8", + }, + ], + "prefixesLength": 0, + "replacementId": undefined, + "sectionHeader": null, + "skip": false, + "sourceFile": "equivalent.asciidoc", + "targetFilename": null, + "tsOptions": {}, + }, ] `; diff --git a/src/test/code-sample.test.ts b/src/test/code-sample.test.ts index bece97f..5da3b8c 100644 --- a/src/test/code-sample.test.ts +++ b/src/test/code-sample.test.ts @@ -555,4 +555,39 @@ describe('addResolvedChecks', () => { // ^? type SynthT2 = "x" | "y" `); }); + + it('should patch a type assertion on a value', () => { + const sample = applyPrefixes( + extractSamples( + dedent` + You can use the same pattern with values as well as types: + + [source,ts] + ---- + function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point (equivalent to "x" | "y") + } + } + ---- + `, + 'equivalent-assertion', + 'source.asciidoc', + ), + ); + + const actual = addResolvedChecks(sample[0]).content; + expect(actual).toEqual(dedent` + function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point + } + } + type Resolve = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]}; + type SynthK = Resolve; + // ^? type SynthK = "x" | "y" + `); + }); }); diff --git a/src/test/inputs/equivalent.asciidoc b/src/test/inputs/equivalent.asciidoc index 0827efd..69abfb4 100644 --- a/src/test/inputs/equivalent.asciidoc +++ b/src/test/inputs/equivalent.asciidoc @@ -40,3 +40,27 @@ type T2 = keyof Point; // ^? type T2 = keyof Point // (equivalent to "x" | "y" | "z") ---- + +You can use the same pattern with values as well as types: + +[source,ts] +---- +function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point (equivalent to "x" | "y") + } +} +---- + +This one should fail: + +[source,ts] +---- +function foo(pt: Point) { + let k: keyof Point; + for (k in pt) { + // ^? let k: keyof Point (equivalent to "x" | "y" | "z") + } +} +----