Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active February 10, 2023 10:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/ec82f0bfe9410b36dcf64a7000b713f6 to your computer and use it in GitHub Desktop.
Save OliverJAsh/ec82f0bfe9410b36dcf64a7000b713f6 to your computer and use it in GitHub Desktop.
import { RequireObjectTypeAnnotations } from '../../rules/RequireObjectTypeAnnotations';
import { ruleTester } from '../../utils';
ruleTester.run('require-object-type-annotations', RequireObjectTypeAnnotations, {
valid: [
{
code: `
const x = {};
`,
},
{
code: `
const x = {
// comment
};
`,
},
{
code: `
const x: { prop: number } = { prop: 1 };
`,
},
{
code: `
const x = { prop: 1 } satisfies { prop: number };
`,
},
{
code: `
const x: { [key: string]: any } = { prop: { prop: 1 } };
`,
},
{
code: `
const x: { [key: string]: unknown } = { prop: { prop: 1 } };
`,
},
{
code: `
const xs: Array<{ prop: number }> = [{ prop: 1 }];
`,
},
{
code: `
const fn: () => { prop: number } = () => ({ prop: 1 });
`,
},
{
code: `
const fn = (): { prop: number } => ({ prop: 1 });
`,
},
{
code: `
declare const f: (x: { prop: number }) => unknown;
f({ prop: 1 });
`,
},
{
code: `
declare const f: { g: (x: { prop: number }) => unknown };
f.g({ prop: 1 });
`,
},
{
code: `
declare const f: <T>(x: { [key: string]: T }) => T;
f({ prop: 1 });
`,
},
{
code: `
declare const mk: <T>() => (t: T) => unknown;
const f = mk<{ prop: number }>();
f({ prop: 1 });
`,
},
{
code: `
declare const f: (arg: () => { prop: number }) => unknown;
f(() => ({ prop: 1 }));
`,
},
{
code: `
interface Base {}
type MyType = Base & {
prop: string;
};
interface A extends MyType {}
interface B extends MyType {}
type Union = A | B;
declare const union: Union;
const v: MyType = { ...union };
`,
},
{
code: `
declare const f: (x: unknown) => unknown;
f({ prop: 1 });
`,
},
{
code: `
[1, 2, 3].map((id): { prop: number } => ({ prop: id }));
`,
},
{
code: `
// eslint-disable-next-line require-object-type-annotations
const obj = { prop: 1 };
declare const f: (t: typeof obj) => unknown;
f({ prop: 1 });
`,
},
// This test should pass but it doesn't due to a bug. Until this bug has been fixed, this test
// is skipped. Once we fix the bug we can uncomment this test.
// {
// code: `
// import * as P from 'fp-ts-routing';
// import { pipe } from 'fp-ts/function';
// pipe(
// P.str('id'),
// P.imap(
// ({ id }): { id2: string } => ({ id2: id }),
// ({ id2 }) => ({ id: id2 }),
// ),
// );
// `,
// },
],
invalid: [
{
code: `
const x = { prop: 1 };
`,
errors: [{ messageId: 'forbidden' }],
},
// Should report error for outer object but not inner object.
{
code: `
const x = { prop: { prop: 1 } };
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
const xs = [{ prop: 1 }];
`,
errors: [{ messageId: 'forbidden' }],
},
// Should report error for outer object but not inner object.
{
code: `
const xs = { ys: [{ prop: 1 }] };
`,
errors: [{ messageId: 'forbidden' }],
},
// Should report error for both outer object and inner object.
{
code: `
const x = {
prop: () => {
const y = { prop: 1 };
return y;
},
};
`,
errors: [{ messageId: 'forbidden' }, { messageId: 'forbidden' }],
},
// Should report error for outer object but not inner object.
{
code: `
const x = {
prop: () => ({ prop: 1 }),
};
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
const fn = () => ({ prop: 1 });
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
const fn = () => ({ prop: 1 });
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
declare const f: <T>(x: T) => T;
f({ prop: 1 });
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
declare const f: <T>(x: T) => T;
const x: { prop: number } = f({ prop: 1 });
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
declare const f: <T extends { prop: 1 }>(x: T) => T;
f({ prop: 1 });
`,
errors: [{ messageId: 'forbidden' }],
},
// Should report error for inner object but not outer object.
{
code: `
declare const f: <T>(x: { [key: string]: T }) => T;
f({ prop: { prop: 1 } });
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
[1, 2, 3].map(id => ({ prop: id }));
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
declare function pipe<A, B>(a: A, ab: (a: A) => B): B;
declare const logName: (user: User) => void;
type User = { name: string };
pipe({ name: "foo" }, logName);
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
const apply =
<T,>(t: T) =>
(f: (t: T) => unknown) =>
f(t);
declare const logName: (user: User) => void;
type User = { name: string };
apply({ name: "foo" })(logName);
`,
errors: [{ messageId: 'forbidden' }],
},
{
code: `
declare const x: { [index: string]: string };
const y = { ...x }
`,
errors: [{ messageId: 'forbidden' }],
},
// This test should fail but it doesn't due to a bug. Until this bug has been fixed, this test
// is skipped. Once we fix the bug we can uncomment this test.
// {
// code: `
// declare const f: <T>(t: T) => T;
// const x: Array<{ name: 'a' | 'b' }> = f([{ name: 'a' }, { name: 'b' }]);
// `,
// errors: [{ messageId: 'forbidden' }],
// },
],
});
import { TSESLint, TSESTree } from '@typescript-eslint/utils';
import * as tsutils from 'tsutils';
import * as ts from 'typescript';
import { getChecker, getParserSvc, ruleCreator } from '../utils';
export const RequireObjectTypeAnnotations = ruleCreator({
defaultOptions: [],
meta: {
docs: {
description:
'Require type annotations for objects where there is no contextual type. See https://gist.github.com/OliverJAsh/268b35729148bd72f8ddbaa4724fbcf4.',
recommended: false,
},
messages: {
forbidden: 'Object is missing type annotation.',
},
schema: [],
type: 'problem',
},
create: (ctx): TSESLint.RuleListener => {
const tc = getChecker(ctx);
const svc = getParserSvc(ctx);
return {
ObjectExpression: (esNode: TSESTree.ObjectExpression): void => {
const tsNode = svc.esTreeNodeToTSNodeMap.get(esNode);
const checkObject = (n: ts.ObjectLiteralExpression): boolean => {
const contextualType = tc.getContextualType(n);
return (
contextualType === undefined ||
/**
* Contextual type is inferred from this object node.
*
* Note: if two nodes are the same node they will be equal by reference.
*
* Examples:
* - object passed as a function argument where the parameter type is generic, e.g.
* `declare const f: <T>(x: T) => T; f({ prop: 1 });``
* - object as a function return value where the return type is generic, e.g.
* `[].map(() => ({ prop: 1 }))`
*/
n === contextualType.getSymbol()?.valueDeclaration
);
};
const checkCanTypeFlowToChild = (n: ts.Node): boolean =>
tsutils.isExpression(n) || ts.isPropertyAssignment(n);
const checkIfReportedForParents = (n: ts.Node): boolean => {
if (checkCanTypeFlowToChild(n) === false) {
return false;
} else {
return ts.isObjectLiteralExpression(n) && checkObject(n)
? true
: checkIfReportedForParents(n.parent);
}
};
const parentEsNode = esNode.parent;
const parentTsNode =
parentEsNode !== undefined ? svc.esTreeNodeToTSNodeMap.get(parentEsNode) : undefined;
if (parentTsNode !== undefined && checkIfReportedForParents(parentTsNode)) {
return;
}
// Allow empty objects
if (tsNode.properties.length === 0) {
return;
}
if (checkObject(tsNode)) {
ctx.report({
node: esNode,
messageId: 'forbidden',
});
}
},
};
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment