From b434f811fee6cef1fa8808810717a0470d0eb94a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 1 Jun 2019 14:13:18 -0400 Subject: [PATCH 1/8] feat(eslint-plugin): added new rule typedef Adds the equivalent of TSLint's `typedef` rule. --- packages/eslint-plugin/README.md | 1 + packages/eslint-plugin/ROADMAP.md | 3 +- packages/eslint-plugin/docs/rules/typedef.md | 364 ++++++++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + packages/eslint-plugin/src/rules/typedef.ts | 242 ++++++++ .../eslint-plugin/tests/rules/typedef.test.ts | 543 ++++++++++++++++++ 7 files changed, 1155 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/docs/rules/typedef.md create mode 100644 packages/eslint-plugin/src/rules/typedef.ts create mode 100644 packages/eslint-plugin/tests/rules/typedef.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 6b605d31b57..957170b8f91 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -163,6 +163,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index b2567afe070..d98853648a2 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -32,7 +32,7 @@ | [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] | | [`prefer-for-of`] | ✅ | [`@typescript-eslint/prefer-for-of`] | | [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | -| [`typedef`] | 🛑 | N/A | +| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | | [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | | [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | @@ -589,6 +589,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/no-unnecessary-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md [`@typescript-eslint/no-var-requires`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md [`@typescript-eslint/type-annotation-spacing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/type-annotation-spacing.md +[`@typescript-eslint/typedef`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/typedef.md [`@typescript-eslint/unified-signatures`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unified-signatures.md [`@typescript-eslint/no-misused-new`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-new.md [`@typescript-eslint/no-object-literal-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md new file mode 100644 index 00000000000..8ef5873b0d0 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/typedef.md @@ -0,0 +1,364 @@ +# Require type annotations to exist (typedef) + +TypeScript cannot always infer types for all places in code. +Some locations require type annotations for their types to be inferred. + +```ts +class ContainsText { + // There must be a type annotation here to infer the type + delayedText: string; + + // `typedef` requires a type annotation here to maintain consistency + immediateTextExplicit: string = 'text'; + + // This is still a string type because of its initial value + immediateTextImplicit = 'text'; +} +``` + +> Note: requiring type annotations unnecessarily can be cumbersome to maintain and generally reduces code readability. +> TypeScript is often better at inferring types than easily written type annotations would allow. +> Instead of enabling `typedef`, it is generally recommended to use the `--noImplicitAny` and/or `--strictPropertyInitialization` compiler options to enforce type annotations only when useful. + +## Rule Details + +This rule can enforce type annotations in locations regardless of whether they're required. +This is typically used to maintain consistency for element types that sometimes require them. + +## Options + +This rule has an object option that may receive any of the following as booleans: + +- `"arrayDestructuring"` +- `"arrowCallSignature"`: `true` by default +- `"arrowParameter"`: `true` by default +- `"callSignature"`: `true` by default +- `"memberVariableDeclaration"`: `true` by default +- `"objectDestructuring"` +- `"parameter"`: `true` by default +- `"propertyDeclaration"`: `true` by default +- `"variableDeclaration"` + +For example, with the following configuration: + +```json +{ + "rules": { + "typedef": [ + "error", + { + "arrowParameter": false, + "variableDeclaration": true + } + ] + } +} +``` + +- Type annotations on arrow function parameters are not required +- Type annotations on variables are required +- Options otherwise adhere to the defaults + +### arrayDestructuring + +Whether to enforce type annotations on variables declared using array destructuring. + +Examples of **incorrect** code with `{ "arrayDestructuring": true }`: + +```ts +const [a] = [1]; +const [b, c] = [1, 2]; +``` + +Examples of **correct** code with `{ "arrayDestructuring": true }`: + +```ts +const [a]: number[] = [1]; +const [b]: [number] = [2]; +const [c, d]: [boolean, string] = [true, 'text']; +``` + +### arrowCallSignature + +Whether to enforce type annotations for return types of arrow functions. + +Examples of **incorrect** code with `{ "arrowCallSignature": true }`: + +```ts +const returnsVoid = () => {}; +const returnsString = () => 'text'; + +['hello', 'world'].map(text => text.length); + +const mapper = { + map: text => text + '...', +}; +``` + +Examples of **correct** code with `{ "arrowCallSignature": true }`: + +```ts +const returnsVoid = (): void => {}; +const returnsString = (): string => 'text'; + +['hello', 'world'].map((text): number => text.length); + +const mapper = { + map: (text): string => text + '...', +}; +``` + +### arrowParameter + +Whether to enforce type annotations for parameters of arrow functions. + +Examples of **incorrect** code with `{ "arrowParameter": true }`: + +```ts +const logsSize = size => console.log(size); + +['hello', 'world'].map(text => text.length); + +const mapper = { + map: text => text + '...', +}; +``` + +Examples of **correct** code with `{ "arrowParameter": true }`: + +```ts +const logsSize = (size: number) => console.log(text); + +['hello', 'world'].map((text: string) => text.length); + +const mapper = { + map: (text: string) => text + '...', +}; +``` + +### callSignature + +Whether to enforce type annotations for return types of functions and methods. + +Examples of **incorrect** code with `{ "callSignature": true }`: + +```ts +function logsSize(size: number) { + console.log(size); +} + +const doublesSize = function(size: number) { + return size * 2; +}; + +const divider = { + dividesSize(size: number) { + return size / 2; + }, + doesNothing: function() { + console.log('...'); + }, +}; + +class Logger { + log(text: string) { + console.log('>', text); + return true; + } +} +``` + +Examples of **correct** code with `{ "callSignature": true }`: + +```ts +function logsSize(size: number): void { + console.log(size); +} + +const doublesSize = function(size: number): number { + return size * 2; +}; + +const divider = { + dividesSize(size: number): number { + return size / 2; + }, + doesNothing: function(): void { + console.log('...'); + }, +}; + +class Logger { + log(text: string): boolean { + console.log('>', text); + return true; + } +} +``` + +### memberVariableDeclaration + +Whether to enforce type annotations on member variables of classes. + +Examples of **incorrect** code with `{ "callSignature": true }`: + +```ts +class ContainsText { + delayedText; + immediateTextImplicit = 'text'; +} +``` + +Examples of **correct** code with `{ "callSignature": true }`: + +```ts +class ContainsText { + delayedText: string; + immediateTextImplicit: string = 'text'; +} +``` + +### objectDestructuring + +Whether to enforce type annotations on variables declared using object destructuring. + +Examples of **incorrect** code with `{ "objectDestructuring": true }`: + +```ts +const { length } = 'text'; +const [b, c] = Math.random() ? [1, 2] : [3, 4]; +``` + +Examples of **correct** code with `{ "objectDestructuring": true }`: + +```ts +const { length }: { length: number } = 'text'; +const [b, c]: [number, number] = Math.random() ? [1, 2] : [3, 4]; +``` + +### parameter + +Whether to enforce type annotations for parameters of functions and methods. + +Examples of **incorrect** code with `{ "parameter": true }`: + +```ts +function logsSize(size): void { + console.log(size); +} + +const doublesSize = function(size): numeber { + return size * 2; +}; + +const divider = { + curriesSize(size): number { + return size; + }, + dividesSize: function(size): number { + return size / 2; + }, +}; + +class Logger { + log(text): boolean { + console.log('>', text); + return true; + } +} +``` + +Examples of **correct** code with `{ "parameter": true }`: + +```ts +function logsSize(size: number): void { + console.log(size); +} + +const doublesSize = function(size: number): numeber { + return size * 2; +}; + +const divider = { + curriesSize(size: number): number { + return size; + }, + dividesSize: function(size: number): number { + return size / 2; + }, +}; + +class Logger { + log(text: boolean): boolean { + console.log('>', text); + return true; + } +} +``` + +### propertyDeclaration + +Whether to enforce type annotations for properties of interfaces and types. + +Examples of **incorrect** code with `{ "propertyDeclaration": true }`: + +```ts +type Members = { + member; + [i: string]: ; +}; + +interface OtherMembers { + otherMember; + [i: number]: ; +} +``` + +Examples of **correct** code with `{ "propertyDeclaration": true }`: + +```ts +type Members = { + member: string; + [i: string]: string | number; +}; + +interface OtherMembers { + otherMember: boolean; + [i: number]: boolean | string; +} +``` + +### variableDeclaration + +Whether to enforce type annotations for variable declarations, excluding array and object destructuring. + +Examples of **incorrect** code with `{ "variableDeclaration": true }`: + +```ts +const text = 'text'; +let initialText = 'text'; +let delayedText; +``` + +Examples of **correct** code with `{ "variableDeclaration": true }`: + +```ts +const text: string = 'text'; +let initialText: string = 'text'; +let delayedText: string; +``` + +## When Not To Use It + +If you are using stricter TypeScript compiler options, particularly `--noImplicitAny` and/or `--strictPropertyInitialization`, you likely don't need this rule. + +In general, if you do not consider the cost of writing unnecessary type annotations reasonable, then do not use this rule. + +## Further Reading + +- [TypeScript Type System](https://basarat.gitbooks.io/typescript/docs/types/type-system.html) +- [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) + +## Compatibility + +- TSLint: [typedef](https://palantir.github.io/tslint/rules/typedef) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index cc2f9778671..d54097be624 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -63,6 +63,7 @@ "semi": "off", "@typescript-eslint/semi": "error", "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/typedef": "error", "@typescript-eslint/unbound-method": "error", "@typescript-eslint/unified-signatures": "error" } diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index d62f58d6af3..1e438ed0c8d 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -49,6 +49,7 @@ import promiseFunctionAsync from './promise-function-async'; import requireArraySortCompare from './require-array-sort-compare'; import restrictPlusOperands from './restrict-plus-operands'; import semi from './semi'; +import typedef from './typedef'; import typeAnnotationSpacing from './type-annotation-spacing'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; @@ -105,6 +106,7 @@ export default { 'require-array-sort-compare': requireArraySortCompare, 'restrict-plus-operands': restrictPlusOperands, semi: semi, + typedef: typedef, 'type-annotation-spacing': typeAnnotationSpacing, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts new file mode 100644 index 00000000000..56f158ce28a --- /dev/null +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -0,0 +1,242 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; +import * as ts from 'typescript'; +import * as util from '../util'; + +interface Options { + arrayDestructuring?: boolean; + arrowCallSignature?: boolean; + arrowParameter?: boolean; + callSignature?: boolean; + memberVariableDeclaration?: boolean; + objectDestructuring?: boolean; + parameter?: boolean; + propertyDeclaration?: boolean; + variableDeclaration?: boolean; +} +type Option = keyof Options; + +const OPTION_ARRAY_DESTRUCTURING: Option = 'arrayDestructuring'; +const OPTION_ARROW_CALL_SIGNATURE: Option = 'arrowCallSignature'; +const OPTION_ARROW_PARAMETER: Option = 'arrowParameter'; +const OPTION_CALL_SIGNATURE: Option = 'callSignature'; +const OPTION_MEMBER_VARIABLE_DECLARATION: Option = 'memberVariableDeclaration'; +const OPTION_OBJECT_DESTRUCTURING: Option = 'objectDestructuring'; +const OPTION_PARAMETER: Option = 'parameter'; +const OPTION_PROPERTY_DECLARATION: Option = 'propertyDeclaration'; +const OPTION_VARIABLE_DECLARATION: Option = 'variableDeclaration'; + +type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; + +export default util.createRule<[Options], MessageIds>({ + name: 'typedef', + meta: { + docs: { + description: 'Requires type annotations to exist', + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + expectedTypedef: 'expected a type annotation', + expectedTypedefNamed: 'expected {{name}} to have a type annotation', + }, + schema: [ + { + type: 'object', + properties: { + [OPTION_ARRAY_DESTRUCTURING]: { type: 'boolean' }, + [OPTION_ARROW_CALL_SIGNATURE]: { type: 'boolean' }, + [OPTION_ARROW_PARAMETER]: { type: 'boolean' }, + [OPTION_CALL_SIGNATURE]: { type: 'boolean' }, + [OPTION_MEMBER_VARIABLE_DECLARATION]: { type: 'boolean' }, + [OPTION_OBJECT_DESTRUCTURING]: { type: 'boolean' }, + [OPTION_PARAMETER]: { type: 'boolean' }, + [OPTION_PROPERTY_DECLARATION]: { type: 'boolean' }, + [OPTION_VARIABLE_DECLARATION]: { type: 'boolean' }, + }, + }, + ], + type: 'suggestion', + }, + defaultOptions: [ + { + [OPTION_ARROW_CALL_SIGNATURE]: true, + [OPTION_ARROW_PARAMETER]: true, + [OPTION_CALL_SIGNATURE]: true, + [OPTION_MEMBER_VARIABLE_DECLARATION]: true, + [OPTION_PARAMETER]: true, + [OPTION_PROPERTY_DECLARATION]: true, + }, + ], + create(context, [options]) { + const parserServices = util.getParserServices(context); + + function report(location: TSESTree.Node, name?: string) { + context.report({ + node: location, + messageId: name ? 'expectedTypedefNamed' : 'expectedTypedef', + data: { name }, + }); + } + + function getEsNodeName(esNode: TSESTree.Parameter | TSESTree.PropertyName) { + return esNode.type === AST_NODE_TYPES.Identifier + ? esNode.name + : undefined; + } + + function getTsNodeName(tsNode: ts.FunctionLike | ts.Identifier) { + if (ts.isIdentifier(tsNode)) { + return tsNode.text; + } + + if (tsNode.name && ts.isIdentifier(tsNode.name)) { + return tsNode.name.text; + } + + return undefined; + } + + function isTypedParameterDeclaration(esNode: TSESTree.Parameter) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) + .parent as ts.ParameterDeclaration; + + return tsNode.type !== undefined; + } + + function isTypedPropertyDeclaration(esNode: TSESTree.Node) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode); + + return ts.isPropertyDeclaration(tsNode) && tsNode.type !== undefined; + } + + function isTypedVariableDeclaration(esNode: TSESTree.Node) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode); + if (!ts.isFunctionLike(tsNode) && !ts.isVariableDeclaration(tsNode)) { + return false; + } + + return tsNode.type !== undefined; + } + + function visitCallSignature( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ) { + if ( + node.parent && + node.parent.type === AST_NODE_TYPES.Property && + node.parent.kind === 'set' + ) { + return; + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get< + ts.FunctionDeclaration | ts.FunctionExpression + >(node); + + if (!tsNode.type) { + report(node, getTsNodeName(tsNode)); + } + } + + return { + ArrayPattern(node) { + if (!options[OPTION_ARRAY_DESTRUCTURING]) { + return; + } + + const parent = node.parent; + if (!parent || isTypedVariableDeclaration(parent)) { + return; + } + + report(node); + }, + ArrowFunctionExpression(node) { + if (options[OPTION_ARROW_CALL_SIGNATURE]) { + const parent = node.parent!; + if ( + parent.type !== AST_NODE_TYPES.CallExpression && + !isTypedPropertyDeclaration(parent) + ) { + report(node); + } + } + + if (options[OPTION_ARROW_PARAMETER]) { + for (const param of node.params) { + if (!isTypedParameterDeclaration(param)) { + report(param, getEsNodeName(param)); + } + } + } + }, + ClassProperty(node) { + if (!options[OPTION_MEMBER_VARIABLE_DECLARATION]) { + return; + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get< + ts.PropertyDeclaration + >(node); + if (tsNode.type !== undefined) { + return; + } + + report( + node, + node.key.type === AST_NODE_TYPES.Identifier + ? node.key.name + : undefined, + ); + }, + 'FunctionDeclaration, FunctionExpression'( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ) { + if (options[OPTION_CALL_SIGNATURE]) { + visitCallSignature(node); + } + + if (options[OPTION_PARAMETER]) { + for (const param of node.params) { + if (!isTypedParameterDeclaration(param)) { + report(param, getEsNodeName(param)); + } + } + } + }, + ObjectPattern(node) { + if (!options[OPTION_OBJECT_DESTRUCTURING]) { + return; + } + + const parent = node.parent; + if (!parent || isTypedVariableDeclaration(parent)) { + return; + } + + report(node); + }, + 'TSIndexSignature, TSPropertySignature'( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + ) { + if (!options[OPTION_PROPERTY_DECLARATION] || node.typeAnnotation) { + return; + } + + report( + node, + node.type === AST_NODE_TYPES.TSPropertySignature + ? getEsNodeName(node.key) + : undefined, + ); + }, + VariableDeclarator(node) { + if (!options[OPTION_VARIABLE_DECLARATION] || node.id.typeAnnotation) { + return; + } + + report(node, getEsNodeName(node.id)); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts new file mode 100644 index 00000000000..dbd3b5fd32a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -0,0 +1,543 @@ +import rule from '../../src/rules/typedef'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, +}); + +ruleTester.run('typedef', rule, { + valid: [ + // Array destructuring + { + code: `const [a]: [number] = [1]`, + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a, b]: [number, number] = [1, 2]`, + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a] = 1;`, + options: [ + { + arrayDestructuring: false, + }, + ], + }, + // Arrow call signatures + `((): void => {})()`, + `((): number => 7)()`, + { + code: `const returns = () => { }`, + options: [ + { + arrowCallSignature: false, + variableDeclaration: false, + }, + ], + }, + // Arrow parameters + `((a: number): void => {})()`, + `((a: string, b: string): void => {})()`, + { + code: `((a: number): void => { })()`, + options: [ + { + arrowCallSignature: false, + arrowParameter: false, + }, + ], + }, + { + code: `((a: string, b: string): void => { })()`, + options: [ + { + arrowCallSignature: false, + arrowParameter: false, + }, + ], + }, + // Call signatures + `function returns(): void { }`, + { + code: `function returns() { }`, + options: [ + { + callSignature: false, + }, + ], + }, + { + code: `const returns = function () { }`, + options: [ + { + callSignature: false, + variableDeclaration: false, + }, + ], + }, + `const container: any = { + get propDef(): number { + return 7; + } + };`, + `const container: any = { + set propDef(input: number) { } + };`, + // Member variable declarations + `class Test { + state: number; + }`, + `class Test { + state: number = 1; + }`, + { + code: `class Test { + state = 1; + }`, + options: [ + { + memberVariableDeclaration: false, + }, + ], + }, + // Object destructuring + { + code: `const { a }: { a: number } = { a: 1 }`, + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a, b }: { [i: string]: number } = { a: 1, b: 2 }`, + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a } = { a: 1 };`, + options: [ + { + objectDestructuring: false, + }, + ], + }, + // Parameters + `function receivesNumber(a: number): void { }`, + `function receivesStrings(a: string, b: string): void { }`, + `function receivesNumber([a]: [number]): void { }`, + `function receivesNumbers([a, b]: number[]): void { }`, + `function receivesString({ a }: { a: string }): void { }`, + `function receivesStrings({ a, b }: { [i: string ]: string }): void { }`, + // Property declarations + `type Test = { + member: number; + };`, + `type Test = { + [i: string]: number; + };`, + `interface Test { + member: string; + };`, + `interface Test { + [i: number]: string; + };`, + { + code: `type Test = { + member; + };`, + options: [ + { + propertyDeclaration: false, + }, + ], + }, + { + code: `type Test = { + [i: string]; + };`, + options: [ + { + propertyDeclaration: false, + }, + ], + }, + // Variable declarations + { + code: `const x: string = "";`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let x: string = "";`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let x: string;`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `const a = 1;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + { + code: `let a;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + { + code: `let a = 1;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + ], + invalid: [ + // Array destructuring + { + code: `const [a] = [1]`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a, b] = [1, 2]`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + arrayDestructuring: true, + }, + ], + }, + // Object destructuring + { + code: `const { a } = { a: 1 }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a, b } = { a: 1, b: 2 }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + objectDestructuring: true, + }, + ], + }, + // Arrow call signatures + { + code: `const returnsVar = () => { }`, + errors: [ + { + column: 20, + messageId: 'expectedTypedef', + }, + ], + }, + // Call signatures + { + code: `function returns() { }`, + errors: [ + { + data: { name: 'returns' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `const returnsVar = function () { }`, + errors: [ + { + column: 20, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `const returnsVar = function returnsExpression () { }`, + errors: [ + { + data: { name: 'returnsExpression' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `const container: any = { + get propDef() { + return 7; + } + };`, + errors: [ + { + data: { name: 'propDef' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + // Member variable declarations + { + code: `class Test { + state = 1; + }`, + errors: [ + { + data: { name: 'state' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `class Test { + ["state"] = 1; + }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + // Parameters + { + code: `function receivesNumber(a): void { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `function receivesStrings(a, b): void { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'b' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `function receivesNumber([a]): void { }`, + errors: [ + { + column: 25, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesNumbers([a, b]): void { }`, + errors: [ + { + column: 26, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesString({ a }): void { }`, + errors: [ + { + column: 25, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesStrings({ a, b }): void { }`, + errors: [ + { + column: 26, + messageId: 'expectedTypedef', + }, + ], + }, + // Property declarations + { + code: `type Test = { + member; + };`, + errors: [ + { + data: { name: 'member' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `type Test = { + [i: string]; + };`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `interface Test { + member; + };`, + errors: [ + { + data: { name: 'member' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `interface Test { + [i: string]; + };`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + // Variable declarations + { + code: `const a = 1;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `const a = 1, b: number = 2, c = 3;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'c' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a = 1;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a = 1, b: number, c = 2;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'c' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + ], +}); From 982d473ad96f60a8f0a4868eb8ea98caf6e71994 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 1 Jun 2019 14:21:56 -0400 Subject: [PATCH 2/8] Always with the docs checking --- packages/eslint-plugin/README.md | 2 +- packages/eslint-plugin/ROADMAP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 957170b8f91..028c3d42707 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -163,8 +163,8 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | -| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | :thought_balloon: | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index d98853648a2..1a4415b43c4 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -32,8 +32,8 @@ | [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] | | [`prefer-for-of`] | ✅ | [`@typescript-eslint/prefer-for-of`] | | [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | -| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | | [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | +| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | | [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | ### Functionality From cf8f0d89a1f9abc08d210e68c05688747472a7d7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 1 Jun 2019 14:59:27 -0400 Subject: [PATCH 3/8] Just a bit more coverage :) --- .../eslint-plugin/tests/rules/typedef.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts index dbd3b5fd32a..50f8e80a3fc 100644 --- a/packages/eslint-plugin/tests/rules/typedef.test.ts +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -38,6 +38,8 @@ ruleTester.run('typedef', rule, { }, ], }, + `let a: number; + [a] = [1];`, // Arrow call signatures `((): void => {})()`, `((): number => 7)()`, @@ -90,6 +92,11 @@ ruleTester.run('typedef', rule, { }, ], }, + `const container: any = { + method(): number { + return 7; + } + };`, `const container: any = { get propDef(): number { return 7; @@ -98,6 +105,11 @@ ruleTester.run('typedef', rule, { `const container: any = { set propDef(input: number) { } };`, + `const container: any = { + ["computed"](): boolean { + return true; + }, + };`, // Member variable declarations `class Test { state: number; @@ -323,6 +335,19 @@ ruleTester.run('typedef', rule, { }, ], }, + { + code: `const container: any = { + method() { + return 7; + } + };`, + errors: [ + { + data: { name: 'method' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, { code: `const container: any = { get propDef() { @@ -336,6 +361,18 @@ ruleTester.run('typedef', rule, { }, ], }, + { + code: `const container: any = { + ["computed"]() { + return true; + }, + };`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, // Member variable declarations { code: `class Test { From 94dfd4c05600e455fa38fd53a628c1443e5973b8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 15 Jun 2019 11:53:53 -0400 Subject: [PATCH 4/8] PR feedback: options enum; valid .md code --- packages/eslint-plugin/docs/rules/typedef.md | 16 +--- packages/eslint-plugin/src/rules/typedef.ts | 84 +++++++++----------- 2 files changed, 42 insertions(+), 58 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md index 8ef5873b0d0..e2aeb9b8860 100644 --- a/packages/eslint-plugin/docs/rules/typedef.md +++ b/packages/eslint-plugin/docs/rules/typedef.md @@ -305,27 +305,17 @@ Examples of **incorrect** code with `{ "propertyDeclaration": true }`: ```ts type Members = { member; - [i: string]: ; -}; - -interface OtherMembers { otherMember; - [i: number]: ; -} +}; ``` Examples of **correct** code with `{ "propertyDeclaration": true }`: ```ts type Members = { - member: string; - [i: string]: string | number; + member: boolean; + otherMember: string; }; - -interface OtherMembers { - otherMember: boolean; - [i: number]: boolean | string; -} ``` ### variableDeclaration diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 56f158ce28a..99c6a11c7e6 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -2,28 +2,19 @@ import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; import * as ts from 'typescript'; import * as util from '../util'; -interface Options { - arrayDestructuring?: boolean; - arrowCallSignature?: boolean; - arrowParameter?: boolean; - callSignature?: boolean; - memberVariableDeclaration?: boolean; - objectDestructuring?: boolean; - parameter?: boolean; - propertyDeclaration?: boolean; - variableDeclaration?: boolean; +const enum OptionKeys { + ArrayDestructuring = 'arrayDestructuring', + ArrowCallSignature = 'arrowCallSignature', + ArrowParameter = 'arrowParameter', + CallSignature = 'callSignature', + MemberVariableDeclaration = 'memberVariableDeclaration', + ObjectDestructuring = 'objectDestructuring', + Parameter = 'parameter', + PropertyDeclaration = 'propertyDeclaration', + VariableDeclaration = 'variableDeclaration', } -type Option = keyof Options; - -const OPTION_ARRAY_DESTRUCTURING: Option = 'arrayDestructuring'; -const OPTION_ARROW_CALL_SIGNATURE: Option = 'arrowCallSignature'; -const OPTION_ARROW_PARAMETER: Option = 'arrowParameter'; -const OPTION_CALL_SIGNATURE: Option = 'callSignature'; -const OPTION_MEMBER_VARIABLE_DECLARATION: Option = 'memberVariableDeclaration'; -const OPTION_OBJECT_DESTRUCTURING: Option = 'objectDestructuring'; -const OPTION_PARAMETER: Option = 'parameter'; -const OPTION_PROPERTY_DECLARATION: Option = 'propertyDeclaration'; -const OPTION_VARIABLE_DECLARATION: Option = 'variableDeclaration'; + +type Options = { [k in OptionKeys]?: boolean }; type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; @@ -43,15 +34,15 @@ export default util.createRule<[Options], MessageIds>({ { type: 'object', properties: { - [OPTION_ARRAY_DESTRUCTURING]: { type: 'boolean' }, - [OPTION_ARROW_CALL_SIGNATURE]: { type: 'boolean' }, - [OPTION_ARROW_PARAMETER]: { type: 'boolean' }, - [OPTION_CALL_SIGNATURE]: { type: 'boolean' }, - [OPTION_MEMBER_VARIABLE_DECLARATION]: { type: 'boolean' }, - [OPTION_OBJECT_DESTRUCTURING]: { type: 'boolean' }, - [OPTION_PARAMETER]: { type: 'boolean' }, - [OPTION_PROPERTY_DECLARATION]: { type: 'boolean' }, - [OPTION_VARIABLE_DECLARATION]: { type: 'boolean' }, + [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, + [OptionKeys.ArrowCallSignature]: { type: 'boolean' }, + [OptionKeys.ArrowParameter]: { type: 'boolean' }, + [OptionKeys.CallSignature]: { type: 'boolean' }, + [OptionKeys.MemberVariableDeclaration]: { type: 'boolean' }, + [OptionKeys.ObjectDestructuring]: { type: 'boolean' }, + [OptionKeys.Parameter]: { type: 'boolean' }, + [OptionKeys.PropertyDeclaration]: { type: 'boolean' }, + [OptionKeys.VariableDeclaration]: { type: 'boolean' }, }, }, ], @@ -59,12 +50,12 @@ export default util.createRule<[Options], MessageIds>({ }, defaultOptions: [ { - [OPTION_ARROW_CALL_SIGNATURE]: true, - [OPTION_ARROW_PARAMETER]: true, - [OPTION_CALL_SIGNATURE]: true, - [OPTION_MEMBER_VARIABLE_DECLARATION]: true, - [OPTION_PARAMETER]: true, - [OPTION_PROPERTY_DECLARATION]: true, + [OptionKeys.ArrowCallSignature]: true, + [OptionKeys.ArrowParameter]: true, + [OptionKeys.CallSignature]: true, + [OptionKeys.MemberVariableDeclaration]: true, + [OptionKeys.Parameter]: true, + [OptionKeys.PropertyDeclaration]: true, }, ], create(context, [options]) { @@ -140,7 +131,7 @@ export default util.createRule<[Options], MessageIds>({ return { ArrayPattern(node) { - if (!options[OPTION_ARRAY_DESTRUCTURING]) { + if (!options[OptionKeys.ArrayDestructuring]) { return; } @@ -152,7 +143,7 @@ export default util.createRule<[Options], MessageIds>({ report(node); }, ArrowFunctionExpression(node) { - if (options[OPTION_ARROW_CALL_SIGNATURE]) { + if (options[OptionKeys.ArrowCallSignature]) { const parent = node.parent!; if ( parent.type !== AST_NODE_TYPES.CallExpression && @@ -162,7 +153,7 @@ export default util.createRule<[Options], MessageIds>({ } } - if (options[OPTION_ARROW_PARAMETER]) { + if (options[OptionKeys.ArrowParameter]) { for (const param of node.params) { if (!isTypedParameterDeclaration(param)) { report(param, getEsNodeName(param)); @@ -171,7 +162,7 @@ export default util.createRule<[Options], MessageIds>({ } }, ClassProperty(node) { - if (!options[OPTION_MEMBER_VARIABLE_DECLARATION]) { + if (!options[OptionKeys.MemberVariableDeclaration]) { return; } @@ -192,11 +183,11 @@ export default util.createRule<[Options], MessageIds>({ 'FunctionDeclaration, FunctionExpression'( node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, ) { - if (options[OPTION_CALL_SIGNATURE]) { + if (options[OptionKeys.CallSignature]) { visitCallSignature(node); } - if (options[OPTION_PARAMETER]) { + if (options[OptionKeys.Parameter]) { for (const param of node.params) { if (!isTypedParameterDeclaration(param)) { report(param, getEsNodeName(param)); @@ -205,7 +196,7 @@ export default util.createRule<[Options], MessageIds>({ } }, ObjectPattern(node) { - if (!options[OPTION_OBJECT_DESTRUCTURING]) { + if (!options[OptionKeys.ObjectDestructuring]) { return; } @@ -219,7 +210,7 @@ export default util.createRule<[Options], MessageIds>({ 'TSIndexSignature, TSPropertySignature'( node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, ) { - if (!options[OPTION_PROPERTY_DECLARATION] || node.typeAnnotation) { + if (!options[OptionKeys.PropertyDeclaration] || node.typeAnnotation) { return; } @@ -231,7 +222,10 @@ export default util.createRule<[Options], MessageIds>({ ); }, VariableDeclarator(node) { - if (!options[OPTION_VARIABLE_DECLARATION] || node.id.typeAnnotation) { + if ( + !options[OptionKeys.VariableDeclaration] || + node.id.typeAnnotation + ) { return; } From 38350c96e79360c74657ce21e5d93339cadb2171 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 15 Jun 2019 12:31:07 -0400 Subject: [PATCH 5/8] Coverage: missing arrow parameter failure tests; isTypedVariableDeclaration type assertion --- packages/eslint-plugin/src/rules/typedef.ts | 7 ++-- .../eslint-plugin/tests/rules/typedef.test.ts | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 99c6a11c7e6..7ae139039ba 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -101,10 +101,9 @@ export default util.createRule<[Options], MessageIds>({ } function isTypedVariableDeclaration(esNode: TSESTree.Node) { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode); - if (!ts.isFunctionLike(tsNode) && !ts.isVariableDeclaration(tsNode)) { - return false; - } + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) as + | ts.FunctionLike + | ts.VariableDeclaration; return tsNode.type !== undefined; } diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts index 50f8e80a3fc..b0579f19565 100644 --- a/packages/eslint-plugin/tests/rules/typedef.test.ts +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -307,6 +307,39 @@ ruleTester.run('typedef', rule, { }, ], }, + // Arrow parameters + { + code: `const receivesNumber = (a): void => { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + arrowCallSignature: false, + }, + ], + }, + { + code: `const receivesStrings = (a, b): void => { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'b' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + arrowCallSignature: false, + }, + ], + }, // Call signatures { code: `function returns() { }`, From e5f12c4cc65ac938118be0047a6f517c1c82c447 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 15 Jul 2019 12:12:29 -0700 Subject: [PATCH 6/8] Removed unnecessary rule options --- packages/eslint-plugin/docs/rules/typedef.md | 98 +------------ packages/eslint-plugin/src/rules/typedef.ts | 58 -------- .../eslint-plugin/tests/rules/typedef.test.ts | 137 ------------------ 3 files changed, 4 insertions(+), 289 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md index e2aeb9b8860..4498ab95610 100644 --- a/packages/eslint-plugin/docs/rules/typedef.md +++ b/packages/eslint-plugin/docs/rules/typedef.md @@ -25,14 +25,14 @@ class ContainsText { This rule can enforce type annotations in locations regardless of whether they're required. This is typically used to maintain consistency for element types that sometimes require them. +> To enforce type definitions existing on call signatures as per TSLint's `arrow-call-signature` and `call-signature` options, use `explicit-function-return-type`. + ## Options This rule has an object option that may receive any of the following as booleans: - `"arrayDestructuring"` -- `"arrowCallSignature"`: `true` by default - `"arrowParameter"`: `true` by default -- `"callSignature"`: `true` by default - `"memberVariableDeclaration"`: `true` by default - `"objectDestructuring"` - `"parameter"`: `true` by default @@ -78,36 +78,6 @@ const [b]: [number] = [2]; const [c, d]: [boolean, string] = [true, 'text']; ``` -### arrowCallSignature - -Whether to enforce type annotations for return types of arrow functions. - -Examples of **incorrect** code with `{ "arrowCallSignature": true }`: - -```ts -const returnsVoid = () => {}; -const returnsString = () => 'text'; - -['hello', 'world'].map(text => text.length); - -const mapper = { - map: text => text + '...', -}; -``` - -Examples of **correct** code with `{ "arrowCallSignature": true }`: - -```ts -const returnsVoid = (): void => {}; -const returnsString = (): string => 'text'; - -['hello', 'world'].map((text): number => text.length); - -const mapper = { - map: (text): string => text + '...', -}; -``` - ### arrowParameter Whether to enforce type annotations for parameters of arrow functions. @@ -136,71 +106,11 @@ const mapper = { }; ``` -### callSignature - -Whether to enforce type annotations for return types of functions and methods. - -Examples of **incorrect** code with `{ "callSignature": true }`: - -```ts -function logsSize(size: number) { - console.log(size); -} - -const doublesSize = function(size: number) { - return size * 2; -}; - -const divider = { - dividesSize(size: number) { - return size / 2; - }, - doesNothing: function() { - console.log('...'); - }, -}; - -class Logger { - log(text: string) { - console.log('>', text); - return true; - } -} -``` - -Examples of **correct** code with `{ "callSignature": true }`: - -```ts -function logsSize(size: number): void { - console.log(size); -} - -const doublesSize = function(size: number): number { - return size * 2; -}; - -const divider = { - dividesSize(size: number): number { - return size / 2; - }, - doesNothing: function(): void { - console.log('...'); - }, -}; - -class Logger { - log(text: string): boolean { - console.log('>', text); - return true; - } -} -``` - ### memberVariableDeclaration Whether to enforce type annotations on member variables of classes. -Examples of **incorrect** code with `{ "callSignature": true }`: +Examples of **incorrect** code with `{ "memberVariableDeclaration": true }`: ```ts class ContainsText { @@ -209,7 +119,7 @@ class ContainsText { } ``` -Examples of **correct** code with `{ "callSignature": true }`: +Examples of **correct** code with `{ "memberVariableDeclaration": true }`: ```ts class ContainsText { diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 7ae139039ba..d65c4f38422 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -4,9 +4,7 @@ import * as util from '../util'; const enum OptionKeys { ArrayDestructuring = 'arrayDestructuring', - ArrowCallSignature = 'arrowCallSignature', ArrowParameter = 'arrowParameter', - CallSignature = 'callSignature', MemberVariableDeclaration = 'memberVariableDeclaration', ObjectDestructuring = 'objectDestructuring', Parameter = 'parameter', @@ -35,9 +33,7 @@ export default util.createRule<[Options], MessageIds>({ type: 'object', properties: { [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, - [OptionKeys.ArrowCallSignature]: { type: 'boolean' }, [OptionKeys.ArrowParameter]: { type: 'boolean' }, - [OptionKeys.CallSignature]: { type: 'boolean' }, [OptionKeys.MemberVariableDeclaration]: { type: 'boolean' }, [OptionKeys.ObjectDestructuring]: { type: 'boolean' }, [OptionKeys.Parameter]: { type: 'boolean' }, @@ -50,9 +46,7 @@ export default util.createRule<[Options], MessageIds>({ }, defaultOptions: [ { - [OptionKeys.ArrowCallSignature]: true, [OptionKeys.ArrowParameter]: true, - [OptionKeys.CallSignature]: true, [OptionKeys.MemberVariableDeclaration]: true, [OptionKeys.Parameter]: true, [OptionKeys.PropertyDeclaration]: true, @@ -75,18 +69,6 @@ export default util.createRule<[Options], MessageIds>({ : undefined; } - function getTsNodeName(tsNode: ts.FunctionLike | ts.Identifier) { - if (ts.isIdentifier(tsNode)) { - return tsNode.text; - } - - if (tsNode.name && ts.isIdentifier(tsNode.name)) { - return tsNode.name.text; - } - - return undefined; - } - function isTypedParameterDeclaration(esNode: TSESTree.Parameter) { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) .parent as ts.ParameterDeclaration; @@ -94,12 +76,6 @@ export default util.createRule<[Options], MessageIds>({ return tsNode.type !== undefined; } - function isTypedPropertyDeclaration(esNode: TSESTree.Node) { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode); - - return ts.isPropertyDeclaration(tsNode) && tsNode.type !== undefined; - } - function isTypedVariableDeclaration(esNode: TSESTree.Node) { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) as | ts.FunctionLike @@ -108,26 +84,6 @@ export default util.createRule<[Options], MessageIds>({ return tsNode.type !== undefined; } - function visitCallSignature( - node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, - ) { - if ( - node.parent && - node.parent.type === AST_NODE_TYPES.Property && - node.parent.kind === 'set' - ) { - return; - } - - const tsNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.FunctionDeclaration | ts.FunctionExpression - >(node); - - if (!tsNode.type) { - report(node, getTsNodeName(tsNode)); - } - } - return { ArrayPattern(node) { if (!options[OptionKeys.ArrayDestructuring]) { @@ -142,16 +98,6 @@ export default util.createRule<[Options], MessageIds>({ report(node); }, ArrowFunctionExpression(node) { - if (options[OptionKeys.ArrowCallSignature]) { - const parent = node.parent!; - if ( - parent.type !== AST_NODE_TYPES.CallExpression && - !isTypedPropertyDeclaration(parent) - ) { - report(node); - } - } - if (options[OptionKeys.ArrowParameter]) { for (const param of node.params) { if (!isTypedParameterDeclaration(param)) { @@ -182,10 +128,6 @@ export default util.createRule<[Options], MessageIds>({ 'FunctionDeclaration, FunctionExpression'( node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, ) { - if (options[OptionKeys.CallSignature]) { - visitCallSignature(node); - } - if (options[OptionKeys.Parameter]) { for (const param of node.params) { if (!isTypedParameterDeclaration(param)) { diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts index b0579f19565..8c281fe5e39 100644 --- a/packages/eslint-plugin/tests/rules/typedef.test.ts +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -40,18 +40,6 @@ ruleTester.run('typedef', rule, { }, `let a: number; [a] = [1];`, - // Arrow call signatures - `((): void => {})()`, - `((): number => 7)()`, - { - code: `const returns = () => { }`, - options: [ - { - arrowCallSignature: false, - variableDeclaration: false, - }, - ], - }, // Arrow parameters `((a: number): void => {})()`, `((a: string, b: string): void => {})()`, @@ -59,7 +47,6 @@ ruleTester.run('typedef', rule, { code: `((a: number): void => { })()`, options: [ { - arrowCallSignature: false, arrowParameter: false, }, ], @@ -68,48 +55,10 @@ ruleTester.run('typedef', rule, { code: `((a: string, b: string): void => { })()`, options: [ { - arrowCallSignature: false, arrowParameter: false, }, ], }, - // Call signatures - `function returns(): void { }`, - { - code: `function returns() { }`, - options: [ - { - callSignature: false, - }, - ], - }, - { - code: `const returns = function () { }`, - options: [ - { - callSignature: false, - variableDeclaration: false, - }, - ], - }, - `const container: any = { - method(): number { - return 7; - } - };`, - `const container: any = { - get propDef(): number { - return 7; - } - };`, - `const container: any = { - set propDef(input: number) { } - };`, - `const container: any = { - ["computed"](): boolean { - return true; - }, - };`, // Member variable declarations `class Test { state: number; @@ -297,16 +246,6 @@ ruleTester.run('typedef', rule, { }, ], }, - // Arrow call signatures - { - code: `const returnsVar = () => { }`, - errors: [ - { - column: 20, - messageId: 'expectedTypedef', - }, - ], - }, // Arrow parameters { code: `const receivesNumber = (a): void => { }`, @@ -316,11 +255,6 @@ ruleTester.run('typedef', rule, { messageId: 'expectedTypedefNamed', }, ], - options: [ - { - arrowCallSignature: false, - }, - ], }, { code: `const receivesStrings = (a, b): void => { }`, @@ -334,77 +268,6 @@ ruleTester.run('typedef', rule, { messageId: 'expectedTypedefNamed', }, ], - options: [ - { - arrowCallSignature: false, - }, - ], - }, - // Call signatures - { - code: `function returns() { }`, - errors: [ - { - data: { name: 'returns' }, - messageId: 'expectedTypedefNamed', - }, - ], - }, - { - code: `const returnsVar = function () { }`, - errors: [ - { - column: 20, - messageId: 'expectedTypedef', - }, - ], - }, - { - code: `const returnsVar = function returnsExpression () { }`, - errors: [ - { - data: { name: 'returnsExpression' }, - messageId: 'expectedTypedefNamed', - }, - ], - }, - { - code: `const container: any = { - method() { - return 7; - } - };`, - errors: [ - { - data: { name: 'method' }, - messageId: 'expectedTypedefNamed', - }, - ], - }, - { - code: `const container: any = { - get propDef() { - return 7; - } - };`, - errors: [ - { - data: { name: 'propDef' }, - messageId: 'expectedTypedefNamed', - }, - ], - }, - { - code: `const container: any = { - ["computed"]() { - return true; - }, - };`, - errors: [ - { - messageId: 'expectedTypedef', - }, - ], }, // Member variable declarations { From 2fffbed72808b9c07a1afb6e995f23203a670571 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 15 Jul 2019 18:51:00 -0700 Subject: [PATCH 7/8] Stopped usage of parser services --- packages/eslint-plugin/src/rules/typedef.ts | 115 +++++++------------- 1 file changed, 37 insertions(+), 78 deletions(-) diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index d65c4f38422..a53f173303c 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -1,5 +1,4 @@ import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; -import * as ts from 'typescript'; import * as util from '../util'; const enum OptionKeys { @@ -53,8 +52,6 @@ export default util.createRule<[Options], MessageIds>({ }, ], create(context, [options]) { - const parserServices = util.getParserServices(context); - function report(location: TSESTree.Node, name?: string) { context.report({ node: location, @@ -63,114 +60,76 @@ export default util.createRule<[Options], MessageIds>({ }); } - function getEsNodeName(esNode: TSESTree.Parameter | TSESTree.PropertyName) { - return esNode.type === AST_NODE_TYPES.Identifier - ? esNode.name - : undefined; - } - - function isTypedParameterDeclaration(esNode: TSESTree.Parameter) { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) - .parent as ts.ParameterDeclaration; - - return tsNode.type !== undefined; + function getNodeName(node: TSESTree.Parameter | TSESTree.PropertyName) { + return node.type === AST_NODE_TYPES.Identifier ? node.name : undefined; } - function isTypedVariableDeclaration(esNode: TSESTree.Node) { - const tsNode = parserServices.esTreeNodeToTSNodeMap.get(esNode) as - | ts.FunctionLike - | ts.VariableDeclaration; - - return tsNode.type !== undefined; + function checkParameters(params: TSESTree.Parameter[]) { + for (const param of params) { + if ( + param.type !== AST_NODE_TYPES.TSParameterProperty && + !param.typeAnnotation + ) { + report(param, getNodeName(param)); + } + } } return { ArrayPattern(node) { - if (!options[OptionKeys.ArrayDestructuring]) { - return; + if (options[OptionKeys.ArrayDestructuring] && !node.typeAnnotation) { + report(node); } - - const parent = node.parent; - if (!parent || isTypedVariableDeclaration(parent)) { - return; - } - - report(node); }, ArrowFunctionExpression(node) { if (options[OptionKeys.ArrowParameter]) { - for (const param of node.params) { - if (!isTypedParameterDeclaration(param)) { - report(param, getEsNodeName(param)); - } - } + checkParameters(node.params); } }, ClassProperty(node) { - if (!options[OptionKeys.MemberVariableDeclaration]) { - return; - } - - const tsNode = parserServices.esTreeNodeToTSNodeMap.get< - ts.PropertyDeclaration - >(node); - if (tsNode.type !== undefined) { - return; + if ( + options[OptionKeys.MemberVariableDeclaration] && + !node.typeAnnotation + ) { + report( + node, + node.key.type === AST_NODE_TYPES.Identifier + ? node.key.name + : undefined, + ); } - - report( - node, - node.key.type === AST_NODE_TYPES.Identifier - ? node.key.name - : undefined, - ); }, 'FunctionDeclaration, FunctionExpression'( node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, ) { if (options[OptionKeys.Parameter]) { - for (const param of node.params) { - if (!isTypedParameterDeclaration(param)) { - report(param, getEsNodeName(param)); - } - } + checkParameters(node.params); } }, ObjectPattern(node) { - if (!options[OptionKeys.ObjectDestructuring]) { - return; + if (options[OptionKeys.ObjectDestructuring] && !node.typeAnnotation) { + report(node); } - - const parent = node.parent; - if (!parent || isTypedVariableDeclaration(parent)) { - return; - } - - report(node); }, 'TSIndexSignature, TSPropertySignature'( node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, ) { - if (!options[OptionKeys.PropertyDeclaration] || node.typeAnnotation) { - return; + if (options[OptionKeys.PropertyDeclaration] && !node.typeAnnotation) { + report( + node, + node.type === AST_NODE_TYPES.TSPropertySignature + ? getNodeName(node.key) + : undefined, + ); } - - report( - node, - node.type === AST_NODE_TYPES.TSPropertySignature - ? getEsNodeName(node.key) - : undefined, - ); }, VariableDeclarator(node) { if ( - !options[OptionKeys.VariableDeclaration] || - node.id.typeAnnotation + options[OptionKeys.VariableDeclaration] && + !node.id.typeAnnotation ) { - return; + report(node, getNodeName(node.id)); } - - report(node, getEsNodeName(node.id)); }, }; }, From 4655c38c1e9f81d0ed4785097c084989e2c4e8c1 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 16 Jul 2019 11:38:47 -0700 Subject: [PATCH 8/8] Removed unnecessary thought balloon --- packages/eslint-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 25940e7cf88..a9d486b76ef 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -180,7 +180,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | | [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | :thought_balloon: | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | |