Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "equivalent to" for values #252

Merged
merged 6 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 25 additions & 7 deletions src/code-sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,27 +300,45 @@ 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> = 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;
if (!content.includes('equivalent to')) {
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<T> 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> = 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,
Expand Down
118 changes: 113 additions & 5 deletions src/test/__snapshots__/asciidoc.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -191,16 +191,68 @@ type SynthT2 = Resolve<T2>;
"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> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};
type SynthK = Resolve<keyof Point>;
// ^? 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",
],
}
`;
Expand Down Expand Up @@ -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": {},
},
]
`;

Expand Down
35 changes: 35 additions & 0 deletions src/test/code-sample.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};
type SynthK = Resolve<keyof Point>;
// ^? type SynthK = "x" | "y"
`);
});
});
24 changes: 24 additions & 0 deletions src/test/inputs/equivalent.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
----