Last active
February 10, 2023 10:20
-
-
Save OliverJAsh/ec82f0bfe9410b36dcf64a7000b713f6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }], | |
// }, | |
], | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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