Skip to content

Instantly share code, notes, and snippets.

@dbalatero
Last active June 19, 2022 22:00
Show Gist options
  • Save dbalatero/82bb17596fe4e8b7d1c5d56d8f6018fb to your computer and use it in GitHub Desktop.
Save dbalatero/82bb17596fe4e8b7d1c5d56d8f6018fb to your computer and use it in GitHub Desktop.
This code lets you find possible bad spots where ts-expect-error is hiding real errors in your code.

There is an issue with // @ts-expect-error where it can hide other errors inside of:

  • React property assignments
type Props = {
  foo: string;
  baz: number;
}

// takes Props
<Component
  // @ts-expect-error - ...
  foo="bar"
  // we're missing a prop `baz`, but that's being hidden
/>
  • Object literals
type MyShape = {
  foo: number;
  baz: string;
}

const blah: MyShape = {
  // @ts-expect-error - expected number, got string
  foo: "bar",
  // missing key `baz`, but we don't see an error
  badKey: "blah", // bad key, but we don't see an error
}

You'd think it would just suppress 1 line, but it ends up suppressing every error inside the component/object declaration.

See this TS playground link for examples of errors being hidden.

Usage

If you want to lint some TS files for problem spots, you can use the attached code which builds an AST of each file and extracts any ts-expect-error comments from the two mentioned contexts above:

import fs from 'fs';

import {lintExpectErrorForFileContents} from './findBadTsExpectErrors.ts';

// You could build this list up with the `glob` library, if you wanted
// and run it on every file in your codebase.
const filesToLint = [
  "src/foo/bar.tsx",
  "src/blah/baz.ts",
];

let foundErrors = false;

filesToLint.forEach((fileName) => {
  const contents = fs.readFileSync(fileName).toString();
  const result = lintExpectErrorForFile(fileName, contents);

  if (!result.isValid()) {
    foundErrors = true;

    result.errors.forEach((error) => {
      const { characterNumber, lineNumber, text } = error;

      // Output path:lineNumber:characterNumber:errorText
      console.log(
        [
          fileName,
          lineNumber,
          characterNumber,
          text,
        ].join(':'),
      )
    });
  }
}

if (!foundErrors) {
  console.log("No bad code spots found, awesome!");
}

This output format should work with Vim's quickfix function, if you want to write a small function to import the results of this command to your quickfix window.

import ts from "typescript";
type ExpectError = {
// What character is the offending statement in?
characterNumber: number;
// The full extracted code context, including comment.
extractedCode: string;
// Where in the file is this comment located?
filePosition: {
startIndex: number;
endIndex: number;
};
// What line number is this error on?
lineNumber: number;
// Text of the comment.
text: string;
};
type FileResultOptions = {
errors: ExpectError[];
};
class FileResult {
errors: ExpectError[];
constructor(options: FileResultOptions) {
this.errors = options.errors;
}
isValid(): boolean {
return this.errors.length === 0;
}
}
// Given a fileName ("blah/foo/bar.ts") and the file contents (the source code),
// lint the entire file and find instances of `@ts-expect-error`.
//
// Returns a FileResult that you can query for errors.
export const lintExpectErrorForFileContents = (
fileName: string,
contents: string
): FileResult => {
if (!contents.includes("ts-expect-error")) {
// Return early if there are no error comments at all.
return new FileResult({ errors: [] });
}
const ast = ts.createSourceFile(fileName, contents, ts.ScriptTarget.Latest);
const errors: ExpectError[] = [];
// Returns the code of a given node, as TypeScript text.
const dumpNode = (node: ts.Node): string => {
return ast.getFullText().slice(node.pos, node.end);
};
// Processes a single node in the AST as we walk it.
const visitNode = (node: ts.Node) => {
// Uncomment to debug each node being used.
// console.log(ts.SyntaxKind[node.kind], dumpNode(node));
switch (node.kind) {
// We either match:
//
// * a property assignment in an object (`bar: "foo"`)
// * a JSX attribute assignment (`bar="foo"`)
//
// and extract the whereabouts of the error.
case ts.SyntaxKind.JsxAttribute:
case ts.SyntaxKind.PropertyAssignment: {
// Find any comments that occur right before this node.
const commentRanges = ts.getLeadingCommentRanges(
ast.getFullText(),
node.getFullStart()
);
if (commentRanges?.length) {
const { line, character } = ast.getLineAndCharacterOfPosition(
node.getStart(ast)
);
commentRanges.forEach((range) => {
const text = ast
.getFullText()
.slice(range.pos, range.end)
.replace(/^\/\/ /, "");
if (text.includes("@ts-expect-error")) {
errors.push({
characterNumber: character + 1,
extractedCode: dumpNode(node),
filePosition: {
startIndex: range.pos,
endIndex: range.end,
},
lineNumber: line, // we want the line to be above the property assignment, where the comment is
text,
});
}
});
}
break;
}
}
ts.forEachChild(node, visitNode);
};
// Walk the entire AST and visit each node; try to extract ts-expect-error
// comments.
visitNode(ast);
// Return whatever result we get back.
return new FileResult({ errors });
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment