From 278a25ac7155fef7a8124fe1050f6211991262ee Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 14:23:10 +1030 Subject: [PATCH] feat: support array and function types --- .../src/rules/naming-convention.ts | 47 ++++++++--- .../tests/rules/naming-convention.test.ts | 77 ++++++++++++++----- .../eslint-plugin/typings/typescript.d.ts | 21 +++++ 3 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 packages/eslint-plugin/typings/typescript.d.ts diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 107ba34f8ce6..63ca9a83feda 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -4,6 +4,7 @@ import { TSESTree, TSESLint, } from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; import * as util from '../util'; type MessageIds = @@ -1138,31 +1139,39 @@ function isCorrectType( const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); const checker = program.getTypeChecker(); const tsNode = esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode); - const typeString = checker.typeToString( - // this will resolve things like true => boolean, 'a' => string and 1 => number - checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), - ); + const type = checker + .getTypeAtLocation(tsNode) + // remove null and undefined from the type, as we don't care about it here + .getNonNullableType(); for (const allowedType of config.types) { switch (allowedType) { case TypeModifiers.array: - // TODO + if ( + isAllTypesMatch( + type, + t => checker.isArrayType(t) || checker.isTupleType(t), + ) + ) { + return true; + } break; case TypeModifiers.function: - // TODO + if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { + return true; + } break; case TypeModifiers.boolean: case TypeModifiers.number: case TypeModifiers.string: { + const typeString = checker.typeToString( + // this will resolve things like true => boolean, 'a' => string and 1 => number + checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), + ); const allowedTypeString = TypeModifiers[allowedType]; - if ( - typeString === `${allowedTypeString}` || - typeString === `${allowedTypeString} | null` || - typeString === `${allowedTypeString} | null | undefined` - ) { + if (typeString === allowedTypeString) { return true; } break; @@ -1173,6 +1182,20 @@ function isCorrectType( return false; } +/** + * @returns `true` if the type (or all union types) in the given type return true for the callback + */ +function isAllTypesMatch( + type: ts.Type, + cb: (type: ts.Type) => boolean, +): boolean { + if (type.isUnion()) { + return type.types.every(t => cb(t)); + } + + return cb(type); +} + export { MessageIds, Options, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index e23a54c9f24b..014149be7b35 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -652,24 +652,24 @@ ruleTester.run('naming-convention', rule, { ...createInvalidTestCases(cases), { code: ` - declare const string_camelCase: string; - declare const string_camelCase: string | null; - declare const string_camelCase: string | null | undefined; - declare const string_camelCase: 'a' | null | undefined; - declare const string_camelCase: string | 'a' | null | undefined; - - declare const number_camelCase: number; - declare const number_camelCase: number | null; - declare const number_camelCase: number | null | undefined; - declare const number_camelCase: 1 | null | undefined; - declare const number_camelCase: number | 2 | null | undefined; - - declare const boolean_camelCase: boolean; - declare const boolean_camelCase: boolean | null; - declare const boolean_camelCase: boolean | null | undefined; - declare const boolean_camelCase: true | null | undefined; - declare const boolean_camelCase: false | null | undefined; - declare const boolean_camelCase: true | false | null | undefined; + declare const string_camelCase01: string; + declare const string_camelCase02: string | null; + declare const string_camelCase03: string | null | undefined; + declare const string_camelCase04: 'a' | null | undefined; + declare const string_camelCase05: string | 'a' | null | undefined; + + declare const number_camelCase06: number; + declare const number_camelCase07: number | null; + declare const number_camelCase08: number | null | undefined; + declare const number_camelCase09: 1 | null | undefined; + declare const number_camelCase10: number | 2 | null | undefined; + + declare const boolean_camelCase11: boolean; + declare const boolean_camelCase12: boolean | null; + declare const boolean_camelCase13: boolean | null | undefined; + declare const boolean_camelCase14: true | null | undefined; + declare const boolean_camelCase15: false | null | undefined; + declare const boolean_camelCase16: true | false | null | undefined; `, options: [ { @@ -694,5 +694,46 @@ ruleTester.run('naming-convention', rule, { parserOptions, errors: Array(16).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + declare const function_camelCase1: (() => void); + declare const function_camelCase2: (() => void) | null; + declare const function_camelCase3: (() => void) | null | undefined; + declare const function_camelCase4: (() => void) | (() => string) | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['function'], + format: ['snake_case'], + prefix: ['function_'], + }, + ], + parserOptions, + errors: Array(4).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + declare const array_camelCase1: Array; + declare const array_camelCase2: ReadonlyArray | null; + declare const array_camelCase3: number[] | null | undefined; + declare const array_camelCase4: readonly number[] | null | undefined; + declare const array_camelCase5: number[] | (number | string)[] | null | undefined; + declare const array_camelCase6: [] | null | undefined; + declare const array_camelCase7: [number] | null | undefined; + + declare const array_camelCase8: readonly number[] | Array | [boolean] | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['array'], + format: ['snake_case'], + prefix: ['array_'], + }, + ], + parserOptions, + errors: Array(8).fill({ messageId: 'doesNotMatchFormat' }), + }, ], }); diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts new file mode 100644 index 000000000000..6d9a098b538b --- /dev/null +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -0,0 +1,21 @@ +import { TypeChecker, Type } from 'typescript'; + +declare module 'typescript' { + interface TypeChecker { + // internal TS APIs + + /** + * @returns `true` if the given type is an array type: + * - Array + * - ReadonlyArray + * - foo[] + * - readonly foo[] + */ + isArrayType(type: Type): boolean; + /** + * @returns `true` if the given type is a tuple type: + * - [foo] + */ + isTupleType(type: Type): boolean; + } +}