Repository: danvk/literate-ts (ts)

Issue:
Support TwoSlash syntax
literate-ts uses its own syntax for type assertions. It reads like English but has dtslint `$ExpectType` semantics:

```ts
let four = 4;  // type is number
```

Now that [TwoSlash](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/ts-twoslasher) is supported on the Playground, it would be a useful alternate syntax:

```ts
let four = 4;
//  ^? let four: number
```

I've already been using it on the Effective TypeScript blog: https://effectivetypescript.com/2022/02/25/gentips-4-display/

See https://github.com/JoshuaKGoldberg/eslint-plugin-expect-type/issues/43
Show diffs on mismatched outputs
When literate-ts runs a code sample through node and there's a diff on the output, it just tells you that:

```
$ yarn verify
...
Logging details to /var/folders/t_/3xnk295j79v51cmlqvtnhslc0000gn/T/tmp-39596CcZ91LJJkOrh/log.txt
Verifying with TypeScript 5.1.6
⠹ 1/1: ./chapters/ch-intro/which-ts.asciidoc: 10/12 ./chapters/ch-intro/which-ts.asciidoc:143
./chapters/ch-intro/which-ts.asciidoc:143: Actual output from Node did not match expected output.
./chapters/ch-intro/which-ts.asciidoc 11/12 passed
 ✗ ./chapters/ch-intro/which-ts.asciidoc:143
✗ 1 / 12 samples failed.
View detailed logs at /var/folders/t_/3xnk295j79v51cmlqvtnhslc0000gn/T/tmp-39596CcZ91LJJkOrh/log.txt
✨  Done in 2.87s.
```

Digging through the detailed logs, you eventually find this:

```
BEGIN #./chapters/ch-intro/which-ts.asciidoc:143

./chapters/ch-intro/which-ts.asciidoc:143: Actual output from Node did not match expected output.
Expected:
/tenses-ts.js:3
tenses[3].toUpperCase();
          ^

Cannot read properties of undefined (reading 'toUpperCase')
----
Actual:
/tenses-ts.js:3
tenses[3].toUpperCase();
          ^

TypeError: Cannot read properties of undefined (reading 'toUpperCase')
----
Actual output matched expected.

END #./chapters/ch-intro/which-ts.asciidoc:143 (1 ms)
```

So the difference is the `TypeError`. It would be helpful if `literate-ts` included a diff in the detailed logs, not to mention directly in the test runner output. Also what's with the "Actual output matched expected" after the mismatch details?

Pull request description:
Add support for Twoslash assertions
Fixes #35 
Fixes #56

Very heavily based on https://github.com/JoshuaKGoldberg/eslint-plugin-expect-type/pull/47

This requires constructing a language service for every code sample. From testing on just chapter 1 of _Effective TypeScript_, this doesn't seem to be an obviously noticeable performance hit.

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index a70085b..97c3fb2 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -3,7 +3,6 @@ module.exports =  {
   extends:  [
     'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from the @typescript-eslint/eslint-plugin
     'prettier/@typescript-eslint',  // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
-    'plugin:prettier/recommended',  // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
   ],
   parserOptions:  {
     ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8ac335a..c691ac5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,8 @@
     "typescript",
     "typescriptreact"
   ],
-  "typescript.tsdk": "node_modules/typescript/lib"
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "[typescript]": {
+    "editor.formatOnSave": true,
+  }
 }
\ No newline at end of file
diff --git a/README.md b/README.md
index 90fed15..28d3f90 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,13 @@ Literate TS checks three sorts of things.
 
    Literate TS will verify that the type is precisely what you specify textually, ala [dtslint][].
 
+   You can also use [twoslash] syntax for type assertions:
+
+   ```ts
+   const parts = 'four score'.split(' ');
+   //    ^? const parts: string[]
+   ```
+
 3. **Output**. To assert the output of a code sample, give it an ID and include a paired one ending
    with `-output`. In Asciidoc, for example:
 
@@ -302,3 +309,4 @@ Publish a new version:
 [lp]: https://en.wikipedia.org/wiki/Literate_programming
 [etsblog]: https://effectivetypescript.com/
 [pylit-post]: https://www.onebigfluke.com/2014/07/how-im-writing-programming-book.html
+[twoslash]: https://shikijs.github.io/twoslash/
diff --git a/examples/asciidoc/sample.asciidoc b/examples/asciidoc/sample.asciidoc
index fa98926..a96196f 100644
--- a/examples/asciidoc/sample.asciidoc
+++ b/examples/asciidoc/sample.asciidoc
@@ -39,15 +39,17 @@ I will occasionally introduce no-op statements to indicate the type of a variabl
 
 [source,ts]
 ----
-function foo(x: string|string[]) {
-  if (Array.isArray(x)) {
-    x;  // type is string[]
+function foo(value: string|string[]) {
+  if (Array.isArray(value)) {
+    value;
+    // ^? (parameter) value: string[]
   } else {
-    x;  // type is string
+    value;
+    // ^? (parameter) value: string
   }
 }
 ----
 
-The `x;` lines are only there to demonstrate the type in each branch of the conditional. You don't need to (and shouldn't) include statements like this in your own code.
+The `value;` lines are only there to demonstrate the type in each branch of the conditional. You don't need to (and shouldn't) include statements like this in your own code.
 
 Unless it's otherwise noted or clear from context, code samples are intended to be checked with the `--strict` flag. All samples were verified using TypeScript 3.5.3.
diff --git a/src/main.ts b/src/main.ts
index 9fd40e6..b2e1f8b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,7 +23,11 @@ import {checkTs, ConfigBundle} from './ts-checker';
 import {CodeSample} from './types';
 import {getTempDir, writeTempFile, fileSlug} from './utils';
 
-const packagePath = path.join(__dirname, '../../package.json');
+const packagePath = path.join(
+  __dirname,
+  // The path to package.json is slightly different when you run via ts-node
+  __dirname.includes('dist') ? '../../package.json' : '../package.json',
+);
 
 const argv = yargs
   .strict()
diff --git a/src/ts-checker.ts b/src/ts-checker.ts
index 29faae9..aaa7085 100644
--- a/src/ts-checker.ts
+++ b/src/ts-checker.ts
@@ -17,11 +17,22 @@ export interface TypeScriptError {
   message: string;
 }
 
-export interface TypeScriptTypeAssertion {
+export interface ExpectTypeAssertion {
   line: number;
   type: string;
 }
 
+export interface TwoslashAssertion {
+  /** Position in the source file that the twoslash assertion points at */
+  position: number;
+  line: number;
+  character: number;
+  /** The expected type in the twoslash comment */
+  type: string;
+}
+
+export type TypeScriptTypeAssertion = ExpectTypeAssertion | TwoslashAssertion;
+
 export interface ConfigBundle {
   options: ts.CompilerOptions;
   host: ts.CompilerHost;
@@ -31,6 +42,11 @@ const COMMENT_PAT = /^( *\/\/) /;
 const TILDE_PAT = / (~+)/g;
 const POST_TILDE_PAT = /\/\/ [~ ]+(?:(.*))?/;
 const TYPE_ASSERTION_PAT = /\/\/.*[tT]ype is (?:still )?(?:just )?(.*)\.?$/;
+const TWOSLASH_PAT = /\/\/ (?: *)\^\? (.*)$/;
+
+function isTwoslashAssertion(a: TypeScriptTypeAssertion): a is TwoslashAssertion {
+  return 'position' in a;
+}
 
 export function extractExpectedErrors(content: string): TypeScriptError[] {
   const lines = content.split('\n');
@@ -139,17 +155,19 @@ function checkMatchingErrors(expectedErrorsIn: TypeScriptError[], actualErrors:
 
 export function hasTypeAssertions(content: string) {
   const lines = content.split('\n');
-  return !!_.find(lines, line => TYPE_ASSERTION_PAT.exec(line));
+  return !!_.find(lines, line => TYPE_ASSERTION_PAT.exec(line) || TWOSLASH_PAT.exec(line));
 }
 
 export function extractTypeAssertions(
   scanner: ts.Scanner,
   source: ts.SourceFile,
 ): TypeScriptTypeAssertion[] {
-  const assertions = [] as TypeScriptTypeAssertion[];
+  const assertions: TypeScriptTypeAssertion[] = [];
+  const lineStarts = source.getLineStarts();
 
   let appliesToPreviousLine = false;
   let colForContinuation = null;
+  let commentPrefixForContinuation = null; // expected whitespace after the "//" for twoslash
   while (scanner.scan() !== ts.SyntaxKind.EndOfFileToken) {
     const token = scanner.getToken();
     if (token === ts.SyntaxKind.WhitespaceTrivia) continue; // ignore leading whitespace.
@@ -167,19 +185,40 @@ export function extractTypeAssertions(
 
     if (token === ts.SyntaxKind.SingleLineCommentTrivia) {
       const commentText = scanner.getTokenText();
-      if (character === colForContinuation) {
+      if (
+        character === colForContinuation &&
+        (commentPrefixForContinuation === null ||
+          commentText.startsWith(commentPrefixForContinuation))
+      ) {
         assertions[assertions.length - 1].type += ' ' + commentText.slice(2).trim();
       } else {
         const type = matchAndExtract(TYPE_ASSERTION_PAT, commentText);
-        if (!type) continue;
-
-        if (appliesToPreviousLine) line -= 1;
-        assertions.push({line, type});
-        colForContinuation = character;
+        if (type) {
+          if (appliesToPreviousLine) line -= 1;
+          assertions.push({line, type});
+          colForContinuation = character;
+        } else {
+          const type = matchAndExtract(TWOSLASH_PAT, commentText);
+          if (!type) continue;
+          if (!appliesToPreviousLine) {
+            throw new Error('Twoslash assertion must be first on line.');
+          }
+          const twoslashOffset = commentText.indexOf('^?');
+          const commentIndex = pos; // position of the "//" in source file
+          const caretIndex = commentIndex + twoslashOffset;
+          // The position of interest is wherever the "^" (caret) is, but on the previous line.
+          const position = caretIndex - (lineStarts[line] - lineStarts[line - 1]);
+          const lineAndChar = source.getLineAndCharacterOfPosition(position);
+          // line and char aren't strictly needed but they make the tests much more readable.
+          assertions.push({position, type, ...lineAndChar});
+          colForContinuation = character;
+          commentPrefixForContinuation = commentText.slice(0, twoslashOffset);
+        }
       }
     } else {
       appliesToPreviousLine = false;
       colForContinuation = null;
+      commentPrefixForContinuation = null;
     }
   }
   return assertions;
@@ -232,11 +271,11 @@ export function typesMatch(expected: string, actual: string) {
   return expected.endsWith('...') && actual.slice(0, n - 3) === expected.slice(0, n - 3);
 }
 
-export function checkTypeAssertions(
+export function checkExpectTypeAssertions(
   source: ts.SourceFile,
   checker: ts.TypeChecker,
-  assertions: TypeScriptTypeAssertion[],
-) {
+  assertions: ExpectTypeAssertion[],
+): boolean {
   const numAssertions = assertions.length;
   let matchedAssertions = 0;
   let anyFailures = false;
@@ -259,13 +298,17 @@ export function checkTypeAssertions(
         const actualType = checker.typeToString(type, nodeForType);
 
         if (!typesMatch(assertion.type, actualType)) {
-          const testedText = node !== nodeForType ? ` (tested ${nodeForType.getText()})` : '';
-          fail(`Failed type assertion for ${node.getText()}${testedText}`);
-          log(`  Expected: ${assertion.type}`);
-          log(`    Actual: ${actualType}`);
+          const testedText = node !== nodeForType ? ` (tested \`${nodeForType.getText()}\`)` : '';
+          fail(
+            `Failed type assertion for \`${node.getText()}\`${testedText}\n` +
+              `  Expected: ${assertion.type}\n` +
+              `    Actual: ${actualType}`,
+          );
           anyFailures = true;
         } else {
-          log(`Type assertion match: ${node.getText()} => ${assertion.type}`);
+          log(`Type assertion match:`);
+          log(`  Expected: ${assertion.type}`);
+          log(`    Actual: ${actualType}`);
           matchedAssertions++;
         }
 
@@ -289,6 +332,112 @@ export function checkTypeAssertions(
   return !anyFailures;
 }
 
+function getNodeAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node | undefined {
+  let candidate: ts.Node | undefined = undefined;
+  ts.forEachChild(sourceFile, function iterate(node) {
+    const start = node.getStart();
+    const end = node.getEnd();
+    if (position >= start && position <= end) {
+      candidate = node;
+      ts.forEachChild(node, iterate);
+    }
+  });
+  return candidate;
+}
+
+function matchModuloWhitespace(actual: string, expected: string): boolean {
+  // TODO: it's much easier to normalize actual based on the displayParts
+  //       This isn't 100% correct if a type has a space in it, e.g. type T = "string literal"
+  const normActual = actual.replace(/[\n\r ]+/g, ' ').trim();
+  const normExpected = expected.replace(/[\n\r ]+/g, ' ').trim();
+  return normActual === normExpected;
+}
+
+export function checkTwoslashAssertions(
+  source: ts.SourceFile,
+  languageService: ts.LanguageService,
+  assertions: TwoslashAssertion[],
+): boolean {
+  let anyFailures = false;
+  let matchedAssertions = 0;
+  for (const assertion of assertions) {
+    const {position, type} = assertion;
+    if (position === -1) {
+      // special case for a twoslash assertion on line 1.
+      fail(`Twoslash assertion on first line: ${type}`);
+      continue;
+    }
+    const node = getNodeAtPosition(source, position);
+    if (!node) {
+      fail(`Unable to find matching node for twoslash assertion: ${type}`);
+      continue;
+    }
+
+    const qi = languageService.getQuickInfoAtPosition(source.fileName, node.getStart());
+    if (!qi?.displayParts) {
+      fail(`Unable to get quickinfo for twoslash assertion ${type}`);
+      continue;
+    }
+    const actual = qi.displayParts.map(dp => dp.text).join('');
+    if (!matchModuloWhitespace(actual, type)) {
+      fail(
+        `Failed type assertion for \`${node.getText()}\`\n` +
+          `  Expected: ${assertion.type}\n` +
+          `    Actual: ${actual}`,
+      );
+      anyFailures = true;
+    } else {
+      log(`Twoslash type assertion match:`);
+      log(`  Expected: ${assertion.type}`);
+      log(`    Actual: ${actual}`);
+      matchedAssertions++;
+    }
+  }
+
+  if (assertions.length) {
+    log(`  ${matchedAssertions}/${assertions.length} twoslash type assertions matched.`);
+  }
+
+  return !anyFailures;
+}
+
+export function checkTypeAssertions(
+  source: ts.SourceFile,
+  checker: ts.TypeChecker,
+  languageService: ts.LanguageService,
+  assertions: TypeScriptTypeAssertion[],
+) {
+  const [twoslashAssertions, expectTypeAssertions] = _.partition(assertions, isTwoslashAssertion);
+  let ok = true;
+  if (expectTypeAssertions.length) {
+    ok = ok && checkExpectTypeAssertions(source, checker, expectTypeAssertions);
+  }
+  if (twoslashAssertions.length) {
+    ok = ok && checkTwoslashAssertions(source, languageService, twoslashAssertions);
+  }
+
+  return ok;
+}
+
+// See https://github.com/JoshuaKGoldberg/eslint-plugin-expect-type/blob/a55413/src/rules/expect.ts#L506-L521
+export function getLanguageServiceHost(program: ts.Program): ts.LanguageServiceHost {
+  return {
+    getCompilationSettings: () => program.getCompilerOptions(),
+    getCurrentDirectory: () => program.getCurrentDirectory(),
+    getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
+    getScriptFileNames: () => program.getSourceFiles().map(sourceFile => sourceFile.fileName),
+    getScriptSnapshot: name =>
+      ts.ScriptSnapshot.fromString(program.getSourceFile(name)?.text ?? ''),
+    getScriptVersion: () => '1',
+    // NB: We can't check `program` for files, it won't contain valid files like package.json
+    fileExists: ts.sys.fileExists,
+    readFile: ts.sys.readFile,
+    readDirectory: ts.sys.readDirectory,
+    directoryExists: ts.sys.directoryExists,
+    getDirectories: ts.sys.getDirectories,
+  };
+}
+
 /** Verify that a TypeScript sample has the expected errors and no others. */
 export async function checkTs(sample: CodeSample, runCode: boolean, config: ConfigBundle) {
   const {id, content} = sample;
@@ -388,7 +537,10 @@ export async function checkTs(sample: CodeSample, runCode: boolean, config: Conf
     const checker = program.getTypeChecker();
 
     const assertions = extractTypeAssertions(scanner, source);
-    ok = ok && checkTypeAssertions(source, checker, assertions);
+    if (assertions.length) {
+      const languageService = ts.createLanguageService(getLanguageServiceHost(program));
+      ok = ok && checkTypeAssertions(source, checker, languageService, assertions);
+    }
   }
 
   if (!ok) {


Test patch:
diff --git a/src/test-tracker.ts b/src/test-tracker.ts
index 12e9ce2..4b1d6ce 100644
--- a/src/test-tracker.ts
+++ b/src/test-tracker.ts
@@ -36,7 +36,7 @@ export function fail(message: string, sample?: CodeSample) {
     sample = currentSample;
   }
 
-  const fullMessage = `${currentSample?.descriptor}: ${message}`;
+  const fullMessage = `💥 ${currentSample?.descriptor}: ${message}`;
   if (!isLoggingToStderr()) {
     console.error('\n' + fullMessage);
   }
diff --git a/src/test/ts-checker.test.ts b/src/test/ts-checker.test.ts
index 0017096..d388a90 100644
--- a/src/test/ts-checker.test.ts
+++ b/src/test/ts-checker.test.ts
@@ -7,6 +7,7 @@ import {
   hasTypeAssertions,
   extractTypeAssertions,
   checkTypeAssertions,
+  getLanguageServiceHost,
 } from '../ts-checker';
 import {dedent} from '../utils';
 
@@ -152,6 +153,14 @@ describe('ts-checker', () => {
       }`),
     ).toBe(true);
 
+    expect(
+      hasTypeAssertions(dedent`
+      for (const xValue of xs) {
+        xValue;
+        // ^? const xValue: number
+      }`),
+    ).toBe(true);
+
     expect(
       hasTypeAssertions(dedent`
       for (const x of xs) {
@@ -160,77 +169,135 @@ describe('ts-checker', () => {
     ).toBe(false);
   });
 
-  test('extractTypeAssertions', () => {
-    const getAssertions = (text: string) => {
-      const testFile = ts.createSourceFile('test.ts', text, ts.ScriptTarget.ES2015);
-      const scanner = ts.createScanner(
-        ts.ScriptTarget.ES2015,
-        false,
-        testFile.languageVariant,
-        testFile.getFullText(),
-      );
-      return extractTypeAssertions(scanner, testFile);
-    };
+  const getAssertions = (text: string) => {
+    const testFile = ts.createSourceFile('test.ts', text, ts.ScriptTarget.ES2015);
+    const scanner = ts.createScanner(
+      ts.ScriptTarget.ES2015,
+      false,
+      testFile.languageVariant,
+      testFile.getFullText(),
+    );
+    return extractTypeAssertions(scanner, testFile);
+  };
 
-    expect(
-      getAssertions(dedent`
-      for (const x of xs) {
-        x;  // type is number
-      }`),
-    ).toEqual([
-      {
-        line: 1,
-        type: 'number',
-      },
-    ]);
+  describe('extractTypeAssertions', () => {
+    test('simple same-line assertion', () => {
+      expect(
+        getAssertions(dedent`
+        for (const x of xs) {
+          x;  // type is number
+        }`),
+      ).toEqual([
+        {
+          line: 1,
+          type: 'number',
+        },
+      ]);
+    });
 
-    expect(
-      getAssertions(dedent`
-      for (const x of xs) {
-        x;  // type is just number
-      }`),
-    ).toEqual([
-      {
-        line: 1,
-        type: 'number',
-      },
-    ]);
+    test('assertion with an adjective', () => {
+      expect(
+        getAssertions(dedent`
+        for (const x of xs) {
+          x;  // type is just number
+        }`),
+      ).toEqual([
+        {
+          line: 1,
+          type: 'number',
+        },
+      ]);
+    });
 
-    expect(
-      getAssertions(dedent`
-      type T = typeof document.getElementById;
-        // type is (elementId: string) => HTMLElement | null
-      `),
-    ).toEqual([
-      {
-        line: 0,
-        type: '(elementId: string) => HTMLElement | null',
-      },
-    ]);
+    test('assertion on on next line', () => {
+      expect(
+        getAssertions(dedent`
+        type T = typeof document.getElementById;
+          // type is (elementId: string) => HTMLElement | null
+        `),
+      ).toEqual([
+        {
+          line: 0,
+          type: '(elementId: string) => HTMLElement | null',
+        },
+      ]);
+    });
 
-    expect(
-      getAssertions(dedent`
-      const o = {x: 1, y: 2};
-      // type is {
-      //   x: number;
-      //   y: number;
-      // }
-      function addWithExtras(a: number, b: number) {
-        const c = a + b;  // type is number
-        // ...
-        return c;
-      }
-      `),
-    ).toEqual([
-      {
-        line: 0,
-        type: '{ x: number; y: number; }',
-      },
-      {
-        line: 6,
-        type: 'number',
-      },
-    ]);
+    test('multiline assertion', () => {
+      expect(
+        getAssertions(dedent`
+        const o = {x: 1, y: 2};
+        // type is {
+        //   x: number;
+        //   y: number;
+        // }
+        function addWithExtras(a: number, b: number) {
+          const c = a + b;  // type is number
+          // ...
+          return c;
+        }
+        `),
+      ).toEqual([
+        {
+          line: 0,
+          type: '{ x: number; y: number; }',
+        },
+        {
+          line: 6,
+          type: 'number',
+        },
+      ]);
+    });
+  });
+
+  describe('extractTypeAssertions twoslash', () => {
+    test('simple twoslash assertion', () => {
+      expect(
+        getAssertions(dedent`
+        for (const xValue of xs) {
+          xValue;
+          // ^? const xValue: number
+        }`),
+      ).toEqual([
+        {
+          line: 1,
+          character: 5,
+          position: 32,
+          type: 'const xValue: number',
+        },
+      ]);
+    });
+
+    test('multiline twoslash assertion', () => {
+      expect(
+        getAssertions(dedent`
+        const o = {x: 1, y: 2};
+        //    ^? const o: {
+        //         x: number;
+        //         y: number;
+        //       }
+        function addWithExtras(a: number, b: number) {
+          const c = a + b;
+          //    ^? const c: number
+          // ...
+          return c;
+        }
+        `),
+      ).toEqual([
+        {
+          line: 0,
+          character: 6,
+          position: 6,
+          type: 'const o: { x: number; y: number; }',
+        },
+        {
+          line: 6,
+          character: 8,
+          position: 154,
+          type: 'const c: number',
+        },
+      ]);
+    });
   });
 
   describe('checkTypeAssertions', () => {
@@ -266,83 +333,144 @@ describe('ts-checker', () => {
       );
 
       const assertions = extractTypeAssertions(scanner, sourceFile);
-      return checkTypeAssertions(sourceFile, program.getTypeChecker(), assertions);
+      const languageService = ts.createLanguageService(getLanguageServiceHost(program));
+      return checkTypeAssertions(sourceFile, program.getTypeChecker(), languageService, assertions);
     };
 
-    test('type assertion on a value', () => {
-      expect(
-        checkAssertions(dedent`
-        const x = 2 + '3';  // ok, x's type is string
-        const y = '2' + 3;  // ok, y's type is string
-      `),
-      ).toBe(true);
-    });
+    describe('ExpectType-style assertions', () => {
+      test('type assertion on a value', () => {
+        expect(
+          checkAssertions(dedent`
+          const x = 2 + '3';  // ok, x's type is string
+          const y = '2' + 3;  // ok, y's type is string
+        `),
+        ).toBe(true);
+      });
 
-    test('type assertion on a type', () => {
-      expect(
-        checkAssertions(dedent`
-        type T = typeof document.getElementById;
-        // type is (elementId: string) => HTMLElement | null
-      `),
-      ).toBe(true);
-    });
+      test('type assertion on a type', () => {
+        expect(
+          checkAssertions(dedent`
+          type T = typeof document.getElementById;
+          // type is (elementId: string) => HTMLElement | null
+        `),
+        ).toBe(true);
+      });
 
-    test('type assertion on a nested value', () => {
-      expect(
-        checkAssertions(dedent`
-        const o = { x: 'a' };
-        o;  // type is { x: string; }
-        o.x;  // type is string
-      `),
-      ).toBe(true);
-    });
+      test('type assertion on a nested value', () => {
+        expect(
+          checkAssertions(dedent`
+          const o = { x: 'a' };
+          o;  // type is { x: string; }
+          o.x;  // type is string
+        `),
+        ).toBe(true);
+      });
 
-    test('type assertion on a multiline value', () => {
-      expect(
-        checkAssertions(dedent`
-        const o = {
-          x: 'a',
-        };  // type is { x: string; }
-      `),
-      ).toBe(true);
-    });
+      test('type assertion on a multiline value', () => {
+        expect(
+          checkAssertions(dedent`
+          const o = {
+            x: 'a',
+          };  // type is { x: string; }
+        `),
+        ).toBe(true);
+      });
 
-    test('type assertion on a call expression', () => {
-      expect(
-        checkAssertions(dedent`
-        const double = (n: number) => ('' + n * n);
-        double(10);  // type is string
-      `),
-      ).toBe(true);
-    });
+      test('type assertion on a call expression', () => {
+        expect(
+          checkAssertions(dedent`
+          const double = (n: number) => ('' + n * n);
+          double(10);  // type is string
+        `),
+        ).toBe(true);
+      });
 
-    test('multiline type assertions', () => {
-      expect(
-        checkAssertions(dedent`
-        const v = {x: 10, y: 20};
-        // type is {
-        //   x: number;
-        //   y: number;
-        // }
-      `),
-      ).toBe(true);
+      test('multiline type assertions', () => {
+        expect(
+          checkAssertions(dedent`
+          const v = {x: 10, y: 20};
+          // type is {
+          //   x: number;
+          //   y: number;
+          // }
+        `),
+        ).toBe(true);
+      });
+
+      test('type assertion with ellipsis', () => {
+        expect(
+          checkAssertions(dedent`
+          const v = { foo: 0, bar: 1, baz: 2, quux: 3 };
+          type T = keyof typeof v;  // type is "foo" | "bar" | "baz" | ...
+          `),
+        ).toBe(true);
+      });
+
+      test('type assertion with excitement', () => {
+        expect(
+          checkAssertions(dedent`
+          type T = ['a', 'b'][number];  // type is "a" | "b"!
+          `),
+        ).toBe(true);
+      });
     });
 
-    test('type assertion with ellipsis', () => {
-      expect(
-        checkAssertions(dedent`
-        const v = { foo: 0, bar: 1, baz: 2, quux: 3 };
-        type T = keyof typeof v;  // type is "foo" | "bar" | "baz" | ...
+    describe('twoslash assertions', () => {
+      test('simple assertions', () => {
+        expect(
+          checkAssertions(dedent`
+          const x = 2 + '3';
+          //    ^? const x: string
+          const y = '2' + 3;
+          //    ^? const y: string
         `),
-      ).toBe(true);
-    });
+        ).toBe(true);
+      });
 
-    test('type assertion with excitement', () => {
-      expect(
-        checkAssertions(dedent`
-        type T = ['a', 'b'][number];  // type is "a" | "b"!
+      test('failing assertion', () => {
+        expect(
+          checkAssertions(dedent`
+          const x = 2 + '3';
+          //    ^? const x: number
+          `),
+        ).toBe(false);
+      });
+
+      test('type assertion on a type', () => {
+        expect(
+          checkAssertions(dedent`
+          type T = typeof document.getElementById;
+          //   ^? type T = (elementId: string) => HTMLElement | null
+        `),
+        ).toBe(true);
+      });
+
+      test('type assertion on a nested value', () => {
+        expect(
+          checkAssertions(dedent`
+          const object = { x: 'a' };
+          object;
+          // ^? const object: { x: string; }
+          object.x;
+          //     ^? (property) x: string
+        `),
+        ).toBe(true);
+      });
+
+      test('type assertion on a multiline value', () => {
+        expect(
+          checkAssertions(dedent`
+          const o = {
+            //  ^? const o: { x: string; }
+            x: 'a',
+          };
+          console.log(o);
+          //          ^? const o: {
+          //               x: string;
+          //             }
         `),
-      ).toBe(true);
+        ).toBe(true);
+      });
     });
 
     // third-party type