From 9a0c28a9257fd73380dbdfa6366b0a9d6db22e66 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Mon, 12 Feb 2024 22:43:06 +0200 Subject: [PATCH 01/66] feat(eslint-plugin): [require-types-exports] add new rule Closes #7670 --- .../src/rules/require-types-exports.ts | 288 ++++ .../tests/rules/require-types-exports.test.ts | 1217 +++++++++++++++++ 2 files changed, 1505 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/require-types-exports.ts create mode 100644 packages/eslint-plugin/tests/rules/require-types-exports.test.ts diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts new file mode 100644 index 000000000000..a972eb707461 --- /dev/null +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -0,0 +1,288 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +type MessageIds = 'requireTypeExport'; + +export default createRule<[], MessageIds>({ + name: 'require-types-exports', + meta: { + type: 'suggestion', + docs: { + recommended: 'strict', + description: + 'Require exporting types that are used in exported functions declarations', + }, + messages: { + requireTypeExport: 'Expected type "{{ name }}" to be exported', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const exportedTypes = new Set(); + const reported = new Set(); + + function visitExportedFunctionDeclaration( + node: TSESTree.ExportNamedDeclaration & { + declaration: TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction; + }, + ): void { + checkFunctionParamsTypes(node.declaration); + checkFunctionReturnType(node.declaration); + } + + function visitExportedVariableDeclaration( + node: TSESTree.ExportNamedDeclaration & { + declaration: TSESTree.VariableDeclaration; + }, + ): void { + node.declaration.declarations.forEach(declaration => { + if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { + checkFunctionParamsTypes(declaration.init); + checkFunctionReturnType(declaration.init); + } + }); + } + + function checkFunctionParamsTypes( + node: + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.ArrowFunctionExpression, + ): void { + node.params.forEach(param => { + getParamTypesNodes(param).forEach(paramTypeNode => { + const name = getTypeName(paramTypeNode); + + if (!name) { + // TODO: Report on the whole function? + return; + } + + const isExported = exportedTypes.has(name); + const isReported = reported.has(name); + + if (isExported || isReported) { + return; + } + + context.report({ + node: paramTypeNode, + messageId: 'requireTypeExport', + data: { + name, + }, + }); + + reported.add(name); + }); + }); + } + + function checkFunctionReturnType( + node: + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.ArrowFunctionExpression, + ): void { + const returnTypeNode = node.returnType; + + if (!returnTypeNode) { + return; + } + + getReturnTypesNodes(returnTypeNode).forEach(returnTypeNode => { + const name = getTypeName(returnTypeNode); + + if (!name) { + return; + } + + const isExported = exportedTypes.has(name); + const isReported = reported.has(name); + + if (isExported || isReported) { + return; + } + + context.report({ + node: returnTypeNode, + messageId: 'requireTypeExport', + data: { + name, + }, + }); + + reported.add(name); + }); + } + + function getParamTypesNodes( + param: TSESTree.Parameter, + ): TSESTree.TSTypeReference[] { + // Single type + if ( + param.type === AST_NODE_TYPES.Identifier && + param.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + return [param.typeAnnotation.typeAnnotation]; + } + + // Union or intersection + if ( + param.type === AST_NODE_TYPES.Identifier && + (param.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSUnionType || + param.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSIntersectionType) + ) { + return param.typeAnnotation.typeAnnotation.types.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + } + + // Tuple + if ( + param.type === AST_NODE_TYPES.ArrayPattern && + param.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSTupleType + ) { + return param.typeAnnotation.typeAnnotation.elementTypes.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + } + + // Inline object + if ( + param.type === AST_NODE_TYPES.ObjectPattern && + param.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeLiteral + ) { + return param.typeAnnotation.typeAnnotation.members.reduce< + TSESTree.TSTypeReference[] + >((acc, member) => { + if ( + member.type === AST_NODE_TYPES.TSPropertySignature && + member.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + acc.push(member.typeAnnotation.typeAnnotation); + } + + return acc; + }, []); + } + + // Rest params + if ( + param.type === AST_NODE_TYPES.RestElement && + param.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSArrayType && + param.typeAnnotation.typeAnnotation.elementType.type === + AST_NODE_TYPES.TSTypeReference + ) { + return [param.typeAnnotation.typeAnnotation.elementType]; + } + + // Default value assignment + if ( + param.type === AST_NODE_TYPES.AssignmentPattern && + param.left.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + return [param.left.typeAnnotation.typeAnnotation]; + } + + return []; + } + + function getReturnTypesNodes( + typeAnnotation: TSESTree.TSTypeAnnotation, + ): TSESTree.TSTypeReference[] { + // Single type + if ( + typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference + ) { + return [typeAnnotation.typeAnnotation]; + } + + // Union or intersection + if ( + typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSUnionType || + typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSIntersectionType + ) { + return typeAnnotation.typeAnnotation.types.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + } + + // Tuple + if (typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTupleType) { + return typeAnnotation.typeAnnotation.elementTypes.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + } + + // Inline object + if (typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeLiteral) { + return typeAnnotation.typeAnnotation.members.reduce< + TSESTree.TSTypeReference[] + >((acc, member) => { + if ( + member.type === AST_NODE_TYPES.TSPropertySignature && + member.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + acc.push(member.typeAnnotation.typeAnnotation); + } + + return acc; + }, []); + } + + return []; + } + + function collectExportedTypes(node: TSESTree.Program): void { + node.body.forEach(statement => { + if (statement.type !== AST_NODE_TYPES.ExportNamedDeclaration) { + return; + } + + const { declaration } = statement; + + if ( + declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration || + declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration + ) { + exportedTypes.add(declaration.id.name); + + return; + } + }); + } + + function getTypeName(typeReference: TSESTree.TSTypeReference): string { + if (typeReference.typeName.type === AST_NODE_TYPES.Identifier) { + return typeReference.typeName.name; + } + + return ''; + } + + return { + Program: collectExportedTypes, + + 'ExportNamedDeclaration[declaration.type="FunctionDeclaration"]': + visitExportedFunctionDeclaration, + + 'ExportNamedDeclaration[declaration.type="TSDeclareFunction"]': + visitExportedFunctionDeclaration, + + 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': + visitExportedVariableDeclaration, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts new file mode 100644 index 000000000000..f8d0437e27f7 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -0,0 +1,1217 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/require-types-exports'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('require-types-exports', rule, { + valid: [ + 'export function f(): void {}', + 'export const f = (): void => {};', + + 'export function f(a: number): void {}', + 'export const f = (a: number): void => {};', + + 'export function f(a: any): void {}', + 'export const f = (a: any): void => {};', + + 'export function f(a: null): void {}', + 'export const f = (a: null): void => {};', + + 'export function f(a: string | number): void {}', + 'export const f = (a: string | number): void => {};', + + 'export function f(a?: string | number): void {}', + 'export const f = (a?: string | number): void => {};', + + 'export function f(a: number): string {}', + 'export const f = (a: number): string => {};', + + 'export function f(...args: any[]): void {}', + 'export const f = (...args: any[]): void => {};', + + 'export function f(...args: unknown[]): void {}', + 'export const f = (...args: unknown[]): void => {};', + + ` + type A = number; + function f(a: A): A { + return a; + } + `, + + ` + type A = number; + const f = (a: A): A => a; + `, + + ` + type A = number; + type B = string; + function f(a: A | B): any { + return a; + } + `, + + ` + type A = number; + type B = string; + const f = (a: A | B): any => a; + `, + + ` + type A = number; + declare function f(a: A): void; + `, + + ` + type A = number; + function f({ a }: { a: A }): A {} + `, + + ` + type A = number; + const f = ({ a }: { a: A }): A => {}; + `, + + ` + type A = number; + type B = string; + function f([a, b]: [A, B]): void {} + `, + + ` + type A = number; + type B = string; + const f = ([a, b]: [A, B]): void => {}; + `, + + ` + type A = number; + function f(a: A): void {} + `, + + ` + type A = number; + const f = (a: A): void => {}; + `, + + ` + interface A { + a: number; + } + + function f(a: A): A { + return a; + } + `, + + ` + interface A { + a: number; + } + + const f = (a: A): A => a; + `, + + ` + export type A = number; + export function f(a: A): void {} + `, + + ` + export type A = number; + export const f = (a: A): void => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(a: A | B): void {} + `, + + ` + export type A = number; + export type B = string; + export const f = (a: A | B): void => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(a: A & B): void {} + `, + + ` + export type A = number; + export type B = string; + export const f = (a: A & B): void => {}; + `, + + ` + export type A = number; + export function f(...args: A[]): void {} + `, + + ` + export type A = number; + export const f = (...args: A[]): void => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(args: { a: A, b: B, c: number }): void {} + `, + + ` + export type A = number; + export type B = string; + export const f = (args: { a: A, b: B, c: number }): void => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(args: [A, B]): void {} + `, + + ` + export type A = number; + export type B = string; + export const f = (args: [A, B]): void => {}; + `, + + ` + export type A = number; + export function f(a: A = 1): void {} + `, + + ` + export type A = number; + export const f = (a: A = 1): void => {}; + `, + + ` + export type A = number; + export function f(): A {} + `, + + ` + export type A = number; + export const f = (): A => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(): A | B {} + `, + + ` + export type A = number; + export type B = string; + export const f = (): A | B => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(): A & B {} + `, + + ` + export type A = number; + export type B = string; + export const f = (): A & B => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(): [A, B] {} + `, + + ` + export type A = number; + export type B = string; + export const f = (): [A, B] => {}; + `, + + ` + export type A = number; + export type B = string; + export function f(): { a: A, b: B } {} + `, + + ` + export type A = number; + export type B = string; + export const f = (): { a: A, b: B } => {}; + `, + ], + + invalid: [ + { + code: ` + type Arg = number; + + export function f(a: Arg): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export const f = (a: Arg): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export function f(a: Arg, b: Arg): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export const f = (a: Arg, b: Arg): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: Arg1, b: Arg2): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 39, + endColumn: 43, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f = (a: Arg1, b: Arg2): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 39, + endColumn: 43, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + + interface Arg2 { + a: string; + } + + export function f(a: Arg1, b: Arg2): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 8, + column: 39, + endColumn: 43, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + + interface Arg2 { + a: string; + } + + export const f = (a: Arg1, b: Arg2): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 8, + column: 39, + endColumn: 43, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: Arg1 | Arg2): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f = (a: Arg1 | Arg2): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: Arg1 & Arg2): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f = (a: Arg1 & Arg2): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f([a, b]: [Arg1, Arg2, number]): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 36, + endColumn: 40, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 42, + endColumn: 46, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + type Arg3 = boolean; + + export function f([a, b]: [Arg1, Arg2, number], c: Arg3): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 36, + endColumn: 40, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 42, + endColumn: 46, + data: { + name: 'Arg2', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 60, + endColumn: 64, + data: { + name: 'Arg3', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f = ([a, b]: [Arg1, Arg2, number]): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 36, + endColumn: 40, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 42, + endColumn: 46, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f({ a, b }: { a: Arg1; b: Arg2; c: number }): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 42, + endColumn: 46, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 51, + endColumn: 55, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f = ({ a, b }: { a: Arg1; b: Arg2; c: number }): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 42, + endColumn: 46, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 51, + endColumn: 55, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export function f(...args: Arg[]): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 36, + endColumn: 39, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export const f = (...args: Arg[]): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 36, + endColumn: 39, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export function f(a: Arg = 1): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export const f = (a: Arg = 1): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Arg', + }, + }, + ], + }, + + // TODO: Find a resaonable way to handle this case + { + code: ` + type Arg = number; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 37, + endColumn: 39, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export const f = (a: T): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 37, + endColumn: 39, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Ret = string; + + export function f(): Ret {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Ret', + }, + }, + ], + }, + + { + code: ` + type Ret = string; + + export const f = (): Ret => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 30, + endColumn: 33, + data: { + name: 'Ret', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): Ret1 | Ret2 {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export const f = (): Ret1 | Ret2 => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): Ret1 & Ret2 {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export const f = (): Ret1 & Ret2 => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 30, + endColumn: 34, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): [Ret1, Ret2, number, Ret1] {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 31, + endColumn: 35, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export const f = (): [Ret1, Ret2, number, Ret1] => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 31, + endColumn: 35, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): { a: Ret1; b: Ret2; c: number; d: Ret1 } {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 35, + endColumn: 39, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export const f = (): { a: Ret1; b: Ret2; c: number; d: Ret1 } => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 35, + endColumn: 39, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export declare function f(a: Arg): void; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 38, + endColumn: 41, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export declare function f(a: Arg): Arg; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 38, + endColumn: 41, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export declare function f(a: Arg1): true; + export declare function f(a: Arg2): false; + export declare function f(a: Arg1 | Arg2): boolean; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 38, + endColumn: 42, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 38, + endColumn: 42, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export const f1 = (a: Arg1): void => {}, + f2 = (a: Arg2): void => {}; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 31, + endColumn: 35, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 20, + endColumn: 24, + data: { + name: 'Arg2', + }, + }, + ], + }, + ], +}); From 7778868a552bfa6a692c6f2a00d6440a957853ec Mon Sep 17 00:00:00 2001 From: StyleShit Date: Mon, 12 Feb 2024 22:53:35 +0200 Subject: [PATCH 02/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index a972eb707461..19df3d44c79a 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -57,7 +57,7 @@ export default createRule<[], MessageIds>({ const name = getTypeName(paramTypeNode); if (!name) { - // TODO: Report on the whole function? + // TODO: Report on the whole function? Is this case even possible? return; } @@ -93,10 +93,11 @@ export default createRule<[], MessageIds>({ return; } - getReturnTypesNodes(returnTypeNode).forEach(returnTypeNode => { + getReturnTypeTypesNodes(returnTypeNode).forEach(returnTypeNode => { const name = getTypeName(returnTypeNode); if (!name) { + // TODO: Report on the whole function? Is this case even possi return; } @@ -198,7 +199,7 @@ export default createRule<[], MessageIds>({ return []; } - function getReturnTypesNodes( + function getReturnTypeTypesNodes( typeAnnotation: TSESTree.TSTypeAnnotation, ): TSESTree.TSTypeReference[] { // Single type From 12fce5b71b7de1cdab294cc48647b399b4ff7464 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Mon, 12 Feb 2024 22:55:53 +0200 Subject: [PATCH 03/66] wip --- .../src/rules/require-types-exports.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 19df3d44c79a..78eb9e1eedca 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -24,6 +24,25 @@ export default createRule<[], MessageIds>({ const exportedTypes = new Set(); const reported = new Set(); + function collectExportedTypes(program: TSESTree.Program): void { + program.body.forEach(statement => { + if (statement.type !== AST_NODE_TYPES.ExportNamedDeclaration) { + return; + } + + const { declaration } = statement; + + if ( + declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration || + declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration + ) { + exportedTypes.add(declaration.id.name); + + return; + } + }); + } + function visitExportedFunctionDeclaration( node: TSESTree.ExportNamedDeclaration & { declaration: TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction; @@ -246,25 +265,6 @@ export default createRule<[], MessageIds>({ return []; } - function collectExportedTypes(node: TSESTree.Program): void { - node.body.forEach(statement => { - if (statement.type !== AST_NODE_TYPES.ExportNamedDeclaration) { - return; - } - - const { declaration } = statement; - - if ( - declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration || - declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration - ) { - exportedTypes.add(declaration.id.name); - - return; - } - }); - } - function getTypeName(typeReference: TSESTree.TSTypeReference): string { if (typeReference.typeName.type === AST_NODE_TYPES.Identifier) { return typeReference.typeName.name; From d62f86c020f208567c6417b02bd941cabcce5e74 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 18:55:43 +0200 Subject: [PATCH 04/66] lint --- packages/eslint-plugin/src/rules/require-types-exports.ts | 2 +- .../tests/rules/require-types-exports.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 78eb9e1eedca..0c0d6ec7026a 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -116,7 +116,7 @@ export default createRule<[], MessageIds>({ const name = getTypeName(returnTypeNode); if (!name) { - // TODO: Report on the whole function? Is this case even possi + // TODO: Report on the whole function? Is this case even possible? return; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index f8d0437e27f7..51f1bcfc82b6 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -170,13 +170,13 @@ ruleTester.run('require-types-exports', rule, { ` export type A = number; export type B = string; - export function f(args: { a: A, b: B, c: number }): void {} + export function f(args: { a: A; b: B; c: number }): void {} `, ` export type A = number; export type B = string; - export const f = (args: { a: A, b: B, c: number }): void => {}; + export const f = (args: { a: A; b: B; c: number }): void => {}; `, ` @@ -250,13 +250,13 @@ ruleTester.run('require-types-exports', rule, { ` export type A = number; export type B = string; - export function f(): { a: A, b: B } {} + export function f(): { a: A; b: B } {} `, ` export type A = number; export type B = string; - export const f = (): { a: A, b: B } => {}; + export const f = (): { a: A; b: B } => {}; `, ], From 0ebebd2919345c86b2244b31069b49583ffb7933 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 19:11:33 +0200 Subject: [PATCH 05/66] wip --- packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/configs/disable-type-checked.ts | 1 + packages/eslint-plugin/src/configs/strict-type-checked.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 ++ packages/eslint-plugin/src/rules/require-types-exports.ts | 1 + 5 files changed, 6 insertions(+) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index b1890165c7ad..f29feca16e3f 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -142,6 +142,7 @@ export = { '@typescript-eslint/require-array-sort-compare': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': 'error', '@typescript-eslint/restrict-template-expressions': 'error', 'no-return-await': 'off', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 09a5c07fd3e7..a0641c0c49fb 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -55,6 +55,7 @@ export = { '@typescript-eslint/promise-function-async': 'off', '@typescript-eslint/require-array-sort-compare': 'off', '@typescript-eslint/require-await': 'off', + '@typescript-eslint/require-types-exports': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 5666c64035da..7bcf3f5920ca 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -71,6 +71,7 @@ export = { '@typescript-eslint/prefer-ts-expect-error': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': 'error', '@typescript-eslint/restrict-template-expressions': 'error', '@typescript-eslint/triple-slash-reference': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index e497019debec..5dc2b88b71c0 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -125,6 +125,7 @@ import promiseFunctionAsync from './promise-function-async'; import quotes from './quotes'; import requireArraySortCompare from './require-array-sort-compare'; import requireAwait from './require-await'; +import requireTypesExports from './require-types-exports'; import restrictPlusOperands from './restrict-plus-operands'; import restrictTemplateExpressions from './restrict-template-expressions'; import returnAwait from './return-await'; @@ -267,6 +268,7 @@ export default { quotes: quotes, 'require-array-sort-compare': requireArraySortCompare, 'require-await': requireAwait, + 'require-types-exports': requireTypesExports, 'restrict-plus-operands': restrictPlusOperands, 'restrict-template-expressions': restrictTemplateExpressions, 'return-await': returnAwait, diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 0c0d6ec7026a..32f178dd95a7 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -11,6 +11,7 @@ export default createRule<[], MessageIds>({ type: 'suggestion', docs: { recommended: 'strict', + requiresTypeChecking: true, description: 'Require exporting types that are used in exported functions declarations', }, From bfee79153753777b133c5b0fe583783b60016fe4 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 19:17:25 +0200 Subject: [PATCH 06/66] spelling... --- .../eslint-plugin/tests/rules/require-types-exports.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 51f1bcfc82b6..d78a12b004d9 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -806,7 +806,7 @@ ruleTester.run('require-types-exports', rule, { ], }, - // TODO: Find a resaonable way to handle this case + // TODO: Find a reasonable way to handle this case { code: ` type Arg = number; From b309b51e1ae3246441e31e77724a5046f850f2d7 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 20:24:39 +0200 Subject: [PATCH 07/66] wip --- .../docs/rules/require-types-exports.md | 75 ++++++++++ .../src/rules/require-types-exports.ts | 59 +++++++- .../tests/rules/require-types-exports.test.ts | 129 ++++++++++++++++++ 3 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/require-types-exports.md diff --git a/packages/eslint-plugin/docs/rules/require-types-exports.md b/packages/eslint-plugin/docs/rules/require-types-exports.md new file mode 100644 index 000000000000..767050deeba0 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/require-types-exports.md @@ -0,0 +1,75 @@ +--- +description: 'Require exporting types that are used in exported functions declarations.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/require-types-exports** for documentation. + +When exporting functions from a module, it is recommended to export also all the +types that are used in the function declarations. This is useful for consumers of +the module, as it allows them to use the types in their own code without having to +use things like [`Parameters`](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype) +or [`ReturnType`](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) to extract the types from the function. + +## Examples + + + +### ❌ Incorrect + +```ts +type Arg = string; +type Result = number; + +export function strLength(arg: Arg): Result { + return arg.length; +} + +interface Fruit { + name: string; + color: string; +} + +export const getFruitName = (fruit: Fruit) => fruit.name; + +enum Color { + Red = 'red', + Green = 'green', + Blue = 'blue', +} + +export declare function getRandomColor(): Color; +``` + +### ✅ Correct + +```ts +export type Arg = string; +export type Result = number; + +export function strLength(arg: Arg): Result { + return arg.length; +} + +export interface Fruit { + name: string; + color: string; +} + +export const getFruitName = (fruit: Fruit) => fruit.name; + +export enum Color { + Red = 'red', + Green = 'green', + Blue = 'blue', +} + +export declare function getRandomColor(): Color; +``` + + + +## When Not To Use It + +When you don't want to enforce exporting types that are used in exported functions declarations. diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 32f178dd95a7..9a65af9ba399 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,7 +1,8 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule } from '../util'; +import { createRule, getParserServices } from '../util'; +import { DefinitionType } from '@typescript-eslint/scope-manager'; type MessageIds = 'requireTypeExport'; @@ -45,7 +46,10 @@ export default createRule<[], MessageIds>({ } function visitExportedFunctionDeclaration( - node: TSESTree.ExportNamedDeclaration & { + node: ( + | TSESTree.ExportNamedDeclaration + | TSESTree.DefaultExportDeclarations + ) & { declaration: TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction; }, ): void { @@ -66,11 +70,46 @@ export default createRule<[], MessageIds>({ }); } + function visitDefaultExportedArrowFunction( + node: TSESTree.ExportDefaultDeclaration & { + declaration: TSESTree.ArrowFunctionExpression; + }, + ): void { + checkFunctionParamsTypes(node.declaration); + checkFunctionReturnType(node.declaration); + } + + function visitDefaultExportedIdentifier( + node: TSESTree.DefaultExportDeclarations & { + declaration: TSESTree.Identifier; + }, + ) { + const scope = context.sourceCode.getScope(node); + const variable = scope.set.get(node.declaration.name); + + if (!variable) { + return; + } + + for (const definition of variable.defs) { + if ( + definition.type === DefinitionType.Variable && + (definition.node.init?.type === + AST_NODE_TYPES.ArrowFunctionExpression || + definition.node.init?.type === AST_NODE_TYPES.FunctionExpression) + ) { + checkFunctionParamsTypes(definition.node.init); + checkFunctionReturnType(definition.node.init); + } + } + } + function checkFunctionParamsTypes( node: | TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction - | TSESTree.ArrowFunctionExpression, + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression, ): void { node.params.forEach(param => { getParamTypesNodes(param).forEach(paramTypeNode => { @@ -105,7 +144,8 @@ export default createRule<[], MessageIds>({ node: | TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction - | TSESTree.ArrowFunctionExpression, + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression, ): void { const returnTypeNode = node.returnType; @@ -285,6 +325,15 @@ export default createRule<[], MessageIds>({ 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': visitExportedVariableDeclaration, + + 'ExportDefaultDeclaration[declaration.type="FunctionDeclaration"]': + visitExportedFunctionDeclaration, + + 'ExportDefaultDeclaration[declaration.type="ArrowFunctionExpression"]': + visitDefaultExportedArrowFunction, + + 'ExportDefaultDeclaration[declaration.type="Identifier"]': + visitDefaultExportedIdentifier, }; }, }); diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index d78a12b004d9..5832d5739c42 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -42,6 +42,9 @@ ruleTester.run('require-types-exports', rule, { 'export function f(...args: unknown[]): void {}', 'export const f = (...args: unknown[]): void => {};', + 'export default function f(): void {}', + 'export default (): void => {};', + ` type A = number; function f(a: A): A { @@ -299,6 +302,44 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Arg = number; + + export default function(a: Arg): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 36, + endColumn: 39, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + export default (a: Arg): void => {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 28, + endColumn: 31, + data: { + name: 'Arg', + }, + }, + ], + }, + { code: ` type Arg = number; @@ -806,6 +847,52 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + export function f(a: Fruit): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 30, + endColumn: 35, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + export const f = (a: Fruit): void => {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 30, + endColumn: 35, + data: { + name: 'Fruit', + }, + }, + ], + }, + // TODO: Find a reasonable way to handle this case { code: ` @@ -1115,6 +1202,48 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Arg = number; + + const a = (a: Arg): void => {}; + + export default a; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 23, + endColumn: 26, + data: { + name: 'Arg', + }, + }, + ], + }, + + { + code: ` + type Arg = number; + + const a = function (a: Arg): void {}; + + export default a; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 32, + endColumn: 35, + data: { + name: 'Arg', + }, + }, + ], + }, + { code: ` type Arg = number; From 6aa6446ed501c801bce0e5af7d161f8a750279c8 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 20:29:52 +0200 Subject: [PATCH 08/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 9a65af9ba399..3614f728828c 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,7 +1,7 @@ import { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule, getParserServices } from '../util'; +import { createRule } from '../util'; import { DefinitionType } from '@typescript-eslint/scope-manager'; type MessageIds = 'requireTypeExport'; From 892c368fdb3787ec561660a8fd05448be1e75620 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 21:08:05 +0200 Subject: [PATCH 09/66] wip --- .../src/rules/require-types-exports.ts | 146 +++++++---- .../tests/rules/require-types-exports.test.ts | 245 +++++++++++++++++- .../require-types-exports.shot | 14 + 3 files changed, 347 insertions(+), 58 deletions(-) create mode 100644 packages/eslint-plugin/tests/schema-snapshots/require-types-exports.shot diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 3614f728828c..ffeafecf9aef 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -6,6 +6,12 @@ import { DefinitionType } from '@typescript-eslint/scope-manager'; type MessageIds = 'requireTypeExport'; +type FunctionNode = + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression; + export default createRule<[], MessageIds>({ name: 'require-types-exports', meta: { @@ -104,16 +110,53 @@ export default createRule<[], MessageIds>({ } } - function checkFunctionParamsTypes( - node: - | TSESTree.FunctionDeclaration - | TSESTree.TSDeclareFunction - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression, - ): void { + function checkFunctionParamsTypes(node: FunctionNode): void { node.params.forEach(param => { - getParamTypesNodes(param).forEach(paramTypeNode => { - const name = getTypeName(paramTypeNode); + getParamTypesNodes(param) + .flatMap(paramTypeNode => { + return convertGenericTypeToTypeReference(node, paramTypeNode); + }) + .forEach(paramTypeNode => { + const name = getTypeName(paramTypeNode); + + if (!name) { + // TODO: Report on the whole function? Is this case even possible? + return; + } + + const isExported = exportedTypes.has(name); + const isReported = reported.has(name); + + if (isExported || isReported) { + return; + } + + context.report({ + node: paramTypeNode, + messageId: 'requireTypeExport', + data: { + name, + }, + }); + + reported.add(name); + }); + }); + } + + function checkFunctionReturnType(node: FunctionNode): void { + const returnTypeNode = node.returnType; + + if (!returnTypeNode) { + return; + } + + getReturnTypeTypesNodes(returnTypeNode) + .flatMap(paramTypeNode => { + return convertGenericTypeToTypeReference(node, paramTypeNode); + }) + .forEach(returnTypeNode => { + const name = getTypeName(returnTypeNode); if (!name) { // TODO: Report on the whole function? Is this case even possible? @@ -128,7 +171,7 @@ export default createRule<[], MessageIds>({ } context.report({ - node: paramTypeNode, + node: returnTypeNode, messageId: 'requireTypeExport', data: { name, @@ -137,47 +180,6 @@ export default createRule<[], MessageIds>({ reported.add(name); }); - }); - } - - function checkFunctionReturnType( - node: - | TSESTree.FunctionDeclaration - | TSESTree.TSDeclareFunction - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression, - ): void { - const returnTypeNode = node.returnType; - - if (!returnTypeNode) { - return; - } - - getReturnTypeTypesNodes(returnTypeNode).forEach(returnTypeNode => { - const name = getTypeName(returnTypeNode); - - if (!name) { - // TODO: Report on the whole function? Is this case even possible? - return; - } - - const isExported = exportedTypes.has(name); - const isReported = reported.has(name); - - if (isExported || isReported) { - return; - } - - context.report({ - node: returnTypeNode, - messageId: 'requireTypeExport', - data: { - name, - }, - }); - - reported.add(name); - }); } function getParamTypesNodes( @@ -306,6 +308,48 @@ export default createRule<[], MessageIds>({ return []; } + function convertGenericTypeToTypeReference( + functionNode: FunctionNode, + typeNode: TSESTree.TSTypeReference, + ): TSESTree.TSTypeReference | TSESTree.TSTypeReference[] { + const typeName = getTypeName(typeNode); + + if (!typeName) { + return typeNode; + } + + const scope = context.sourceCode.getScope(functionNode); + const variable = scope.set.get(typeName); + + if (!variable || !variable.isTypeVariable) { + return typeNode; + } + + for (const definition of variable.defs) { + if ( + definition.type === DefinitionType.Type && + definition.node.type === AST_NODE_TYPES.TSTypeParameter && + definition.node.constraint + ) { + switch (definition.node.constraint.type) { + case AST_NODE_TYPES.TSTypeReference: + return definition.node.constraint; + + case AST_NODE_TYPES.TSUnionType: + case AST_NODE_TYPES.TSIntersectionType: + return definition.node.constraint.types.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + + default: + continue; + } + } + } + + return typeNode; + } + function getTypeName(typeReference: TSESTree.TSTypeReference): string { if (typeReference.typeName.type === AST_NODE_TYPES.Identifier) { return typeReference.typeName.name; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 5832d5739c42..6bdc65c1dda1 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -893,7 +893,6 @@ ruleTester.run('require-types-exports', rule, { ], }, - // TODO: Find a reasonable way to handle this case { code: ` type Arg = number; @@ -905,7 +904,7 @@ ruleTester.run('require-types-exports', rule, { messageId: 'requireTypeExport', line: 4, column: 37, - endColumn: 39, + endColumn: 40, data: { name: 'Arg', }, @@ -915,18 +914,115 @@ ruleTester.run('require-types-exports', rule, { { code: ` - type Arg = number; + type Arg1 = number; + type Arg2 = string; - export const f = (a: T): void => {}; + export function f(a: T): void {} `, errors: [ { messageId: 'requireTypeExport', - line: 4, + line: 5, column: 37, - endColumn: 39, + endColumn: 41, data: { - name: 'Arg', + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Arg2', + }, + }, + ], + }, + + { + code: ` + type Arg1 = number; + type Arg2 = string; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Arg2', }, }, ], @@ -1202,6 +1298,141 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Ret = string; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 37, + endColumn: 40, + data: { + name: 'Ret', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + + { + code: ` + type Ret1 = string; + type Ret2 = number; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 41, + data: { + name: 'Ret1', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 44, + endColumn: 48, + data: { + name: 'Ret2', + }, + }, + ], + }, + { code: ` type Arg = number; diff --git a/packages/eslint-plugin/tests/schema-snapshots/require-types-exports.shot b/packages/eslint-plugin/tests/schema-snapshots/require-types-exports.shot new file mode 100644 index 000000000000..2f0fbf6d8dfc --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/require-types-exports.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes require-types-exports 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; From 0e8e58fc52567ec571b014598a345d6814e31e71 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 21:15:26 +0200 Subject: [PATCH 10/66] tuple generic --- .../src/rules/require-types-exports.ts | 9 +++++ .../tests/rules/require-types-exports.test.ts | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index ffeafecf9aef..b4b8eb5f3b66 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -332,15 +332,24 @@ export default createRule<[], MessageIds>({ definition.node.constraint ) { switch (definition.node.constraint.type) { + // T extends SomeType case AST_NODE_TYPES.TSTypeReference: return definition.node.constraint; + // T extends SomeType | AnotherType + // T extends SomeType & AnotherType case AST_NODE_TYPES.TSUnionType: case AST_NODE_TYPES.TSIntersectionType: return definition.node.constraint.types.filter( type => type.type === AST_NODE_TYPES.TSTypeReference, ) as TSESTree.TSTypeReference[]; + // T extends [SomeType, AnotherType] + case AST_NODE_TYPES.TSTupleType: + return definition.node.constraint.elementTypes.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + default: continue; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 6bdc65c1dda1..a90aa473964b 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -1028,6 +1028,25 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Arg = string; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 38, + endColumn: 41, + data: { + name: 'Arg', + }, + }, + ], + }, + { code: ` type Ret = string; @@ -1433,6 +1452,25 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Ret = string; + + export function f(): T {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 38, + endColumn: 41, + data: { + name: 'Ret', + }, + }, + ], + }, + { code: ` type Arg = number; From f4018a8f43603800f3113a4fceacd3c83c11f136 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 21:37:17 +0200 Subject: [PATCH 11/66] wip --- .../src/rules/require-types-exports.ts | 16 ++++++++++++++ .../tests/rules/require-types-exports.test.ts | 21 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index b4b8eb5f3b66..4e208e813d7b 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -350,6 +350,22 @@ export default createRule<[], MessageIds>({ type => type.type === AST_NODE_TYPES.TSTypeReference, ) as TSESTree.TSTypeReference[]; + // T extends { some: SomeType, another: AnotherType } + case AST_NODE_TYPES.TSTypeLiteral: + return definition.node.constraint.members.reduce< + TSESTree.TSTypeReference[] + >((acc, member) => { + if ( + member.type === AST_NODE_TYPES.TSPropertySignature && + member.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + acc.push(member.typeAnnotation.typeAnnotation); + } + + return acc; + }, []); + default: continue; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index a90aa473964b..0a8833f44c07 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -1004,7 +1004,7 @@ ruleTester.run('require-types-exports', rule, { type Arg1 = number; type Arg2 = string; - export function f(a: T): void {} + export const f = (a: T): void => {} `, errors: [ { @@ -1047,6 +1047,25 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Arg = string; + + export function f(a: T): void {} + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 42, + endColumn: 45, + data: { + name: 'Arg', + }, + }, + ], + }, + { code: ` type Ret = string; From 89a8344b63b03d0dd2689266637af5f419e27d58 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 13 Feb 2024 22:11:48 +0200 Subject: [PATCH 12/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 4e208e813d7b..281b204556cf 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -321,7 +321,7 @@ export default createRule<[], MessageIds>({ const scope = context.sourceCode.getScope(functionNode); const variable = scope.set.get(typeName); - if (!variable || !variable.isTypeVariable) { + if (!variable?.isTypeVariable) { return typeNode; } From b2138e36213a51de1f750390ef4394a2364d3cfd Mon Sep 17 00:00:00 2001 From: StyleShit Date: Thu, 15 Feb 2024 16:01:13 +0200 Subject: [PATCH 13/66] wip --- .../src/rules/require-types-exports.ts | 51 +++++++++---------- .../tests/rules/require-types-exports.test.ts | 36 ++++++++++--- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 281b204556cf..0f55054e3b06 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,8 +1,8 @@ -import { TSESTree } from '@typescript-eslint/utils'; +import { DefinitionType } from '@typescript-eslint/scope-manager'; +import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -import { DefinitionType } from '@typescript-eslint/scope-manager'; type MessageIds = 'requireTypeExport'; @@ -29,26 +29,17 @@ export default createRule<[], MessageIds>({ }, defaultOptions: [], create(context) { - const exportedTypes = new Set(); - const reported = new Set(); - - function collectExportedTypes(program: TSESTree.Program): void { - program.body.forEach(statement => { - if (statement.type !== AST_NODE_TYPES.ExportNamedDeclaration) { - return; - } + const externalizedTypes = new Set(); + const reportedTypes = new Set(); - const { declaration } = statement; - - if ( - declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration || - declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration - ) { - exportedTypes.add(declaration.id.name); + function collectImportedTypes(node: TSESTree.ImportSpecifier): void { + externalizedTypes.add(node.local.name); + } - return; - } - }); + function collectExportedTypes( + node: TSESTree.TSTypeAliasDeclaration | TSESTree.TSInterfaceDeclaration, + ): void { + externalizedTypes.add(node.id.name); } function visitExportedFunctionDeclaration( @@ -89,7 +80,7 @@ export default createRule<[], MessageIds>({ node: TSESTree.DefaultExportDeclarations & { declaration: TSESTree.Identifier; }, - ) { + ): void { const scope = context.sourceCode.getScope(node); const variable = scope.set.get(node.declaration.name); @@ -124,8 +115,8 @@ export default createRule<[], MessageIds>({ return; } - const isExported = exportedTypes.has(name); - const isReported = reported.has(name); + const isExported = externalizedTypes.has(name); + const isReported = reportedTypes.has(name); if (isExported || isReported) { return; @@ -139,7 +130,7 @@ export default createRule<[], MessageIds>({ }, }); - reported.add(name); + reportedTypes.add(name); }); }); } @@ -163,8 +154,8 @@ export default createRule<[], MessageIds>({ return; } - const isExported = exportedTypes.has(name); - const isReported = reported.has(name); + const isExported = externalizedTypes.has(name); + const isReported = reportedTypes.has(name); if (isExported || isReported) { return; @@ -178,7 +169,7 @@ export default createRule<[], MessageIds>({ }, }); - reported.add(name); + reportedTypes.add(name); }); } @@ -384,7 +375,11 @@ export default createRule<[], MessageIds>({ } return { - Program: collectExportedTypes, + 'ImportDeclaration[importKind="type"] ImportSpecifier, ImportSpecifier[importKind="type"]': + collectImportedTypes, + + 'ExportNamedDeclaration TSTypeAliasDeclaration, ExportNamedDeclaration TSInterfaceDeclaration': + collectExportedTypes, 'ExportNamedDeclaration[declaration.type="FunctionDeclaration"]': visitExportedFunctionDeclaration, diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 0a8833f44c07..c83b3b893413 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -261,6 +261,30 @@ ruleTester.run('require-types-exports', rule, { export type B = string; export const f = (): { a: A; b: B } => {}; `, + + ` + import { testFunction, type Arg } from './module'; + + export function f(a: Arg): void {} + `, + + ` + import type { Arg } from './types'; + + export function f(a: Arg): void {} + `, + + ` + import type { ImportedArg as Arg } from './types'; + + export function f(a: Arg): void {} + `, + + ` + import type { Arg } from './types'; + + export function f(a: T): void {} + `, ], invalid: [ @@ -306,14 +330,14 @@ ruleTester.run('require-types-exports', rule, { code: ` type Arg = number; - export default function(a: Arg): void {} + export default function (a: Arg): void {} `, errors: [ { messageId: 'requireTypeExport', line: 4, - column: 36, - endColumn: 39, + column: 37, + endColumn: 40, data: { name: 'Arg', }, @@ -325,7 +349,7 @@ ruleTester.run('require-types-exports', rule, { code: ` type Arg = number; - export default (a: Arg): void => {} + export default (a: Arg): void => {}; `, errors: [ { @@ -878,7 +902,7 @@ ruleTester.run('require-types-exports', rule, { Cherry, } - export const f = (a: Fruit): void => {} + export const f = (a: Fruit): void => {}; `, errors: [ { @@ -1004,7 +1028,7 @@ ruleTester.run('require-types-exports', rule, { type Arg1 = number; type Arg2 = string; - export const f = (a: T): void => {} + export const f = (a: T): void => {}; `, errors: [ { From 1161db04d32ea6039c4646b096d3817dbe59de02 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 16 Feb 2024 14:15:10 +0200 Subject: [PATCH 14/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 0f55054e3b06..681ee9d16757 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -115,10 +115,10 @@ export default createRule<[], MessageIds>({ return; } - const isExported = externalizedTypes.has(name); + const isExternalized = externalizedTypes.has(name); const isReported = reportedTypes.has(name); - if (isExported || isReported) { + if (isExternalized || isReported) { return; } @@ -154,10 +154,10 @@ export default createRule<[], MessageIds>({ return; } - const isExported = externalizedTypes.has(name); + const isExternalized = externalizedTypes.has(name); const isReported = reportedTypes.has(name); - if (isExported || isReported) { + if (isExternalized || isReported) { return; } From d9875b32873e8cbaf87749fc35a37c1a915d99de Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 16 Feb 2024 14:22:47 +0200 Subject: [PATCH 15/66] refactor --- .../src/rules/require-types-exports.ts | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 681ee9d16757..a71f9c09ca0c 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -102,55 +102,16 @@ export default createRule<[], MessageIds>({ } function checkFunctionParamsTypes(node: FunctionNode): void { - node.params.forEach(param => { - getParamTypesNodes(param) - .flatMap(paramTypeNode => { - return convertGenericTypeToTypeReference(node, paramTypeNode); - }) - .forEach(paramTypeNode => { - const name = getTypeName(paramTypeNode); - - if (!name) { - // TODO: Report on the whole function? Is this case even possible? - return; - } - - const isExternalized = externalizedTypes.has(name); - const isReported = reportedTypes.has(name); - - if (isExternalized || isReported) { - return; - } - - context.report({ - node: paramTypeNode, - messageId: 'requireTypeExport', - data: { - name, - }, - }); - - reportedTypes.add(name); - }); - }); - } - - function checkFunctionReturnType(node: FunctionNode): void { - const returnTypeNode = node.returnType; - - if (!returnTypeNode) { - return; - } + for (const param of node.params) { + const typeNodes = getParamTypesNodes(param).flatMap(typeNode => { + return convertGenericTypeToTypeReferences(node, typeNode); + }); - getReturnTypeTypesNodes(returnTypeNode) - .flatMap(paramTypeNode => { - return convertGenericTypeToTypeReference(node, paramTypeNode); - }) - .forEach(returnTypeNode => { - const name = getTypeName(returnTypeNode); + for (const typeNode of typeNodes) { + const name = getTypeName(typeNode); if (!name) { - // TODO: Report on the whole function? Is this case even possible? + // TODO: Report the whole function? Is this case even possible? return; } @@ -162,7 +123,7 @@ export default createRule<[], MessageIds>({ } context.report({ - node: returnTypeNode, + node: typeNode, messageId: 'requireTypeExport', data: { name, @@ -170,7 +131,48 @@ export default createRule<[], MessageIds>({ }); reportedTypes.add(name); + } + } + } + + function checkFunctionReturnType(node: FunctionNode): void { + const { returnType } = node; + + if (!returnType) { + return; + } + + const typeNodes = getReturnTypeTypesNodes(returnType).flatMap( + typeNode => { + return convertGenericTypeToTypeReferences(node, typeNode); + }, + ); + + for (const typeNode of typeNodes) { + const name = getTypeName(typeNode); + + if (!name) { + // TODO: Report the whole function? Is this case even possible? + return; + } + + const isExternalized = externalizedTypes.has(name); + const isReported = reportedTypes.has(name); + + if (isExternalized || isReported) { + return; + } + + context.report({ + node: typeNode, + messageId: 'requireTypeExport', + data: { + name, + }, }); + + reportedTypes.add(name); + } } function getParamTypesNodes( @@ -299,7 +301,7 @@ export default createRule<[], MessageIds>({ return []; } - function convertGenericTypeToTypeReference( + function convertGenericTypeToTypeReferences( functionNode: FunctionNode, typeNode: TSESTree.TSTypeReference, ): TSESTree.TSTypeReference | TSESTree.TSTypeReference[] { From 6338202219bc9f38172b33a45ca8624e9dcffaa6 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 16 Feb 2024 14:42:09 +0200 Subject: [PATCH 16/66] make it shorter & more readable --- .../src/rules/require-types-exports.ts | 180 ++++++++---------- 1 file changed, 79 insertions(+), 101 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index a71f9c09ca0c..66347fb95569 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -59,12 +59,12 @@ export default createRule<[], MessageIds>({ declaration: TSESTree.VariableDeclaration; }, ): void { - node.declaration.declarations.forEach(declaration => { + for (const declaration of node.declaration.declarations) { if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { checkFunctionParamsTypes(declaration.init); checkFunctionReturnType(declaration.init); } - }); + } } function visitDefaultExportedArrowFunction( @@ -88,49 +88,26 @@ export default createRule<[], MessageIds>({ return; } - for (const definition of variable.defs) { + for (const def of variable.defs) { if ( - definition.type === DefinitionType.Variable && - (definition.node.init?.type === - AST_NODE_TYPES.ArrowFunctionExpression || - definition.node.init?.type === AST_NODE_TYPES.FunctionExpression) + def.type === DefinitionType.Variable && + (def.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression || + def.node.init?.type === AST_NODE_TYPES.FunctionExpression) ) { - checkFunctionParamsTypes(definition.node.init); - checkFunctionReturnType(definition.node.init); + checkFunctionParamsTypes(def.node.init); + checkFunctionReturnType(def.node.init); } } } function checkFunctionParamsTypes(node: FunctionNode): void { for (const param of node.params) { - const typeNodes = getParamTypesNodes(param).flatMap(typeNode => { + const typeNodes = getParamTypeNodes(param).flatMap(typeNode => { return convertGenericTypeToTypeReferences(node, typeNode); }); for (const typeNode of typeNodes) { - const name = getTypeName(typeNode); - - if (!name) { - // TODO: Report the whole function? Is this case even possible? - return; - } - - const isExternalized = externalizedTypes.has(name); - const isReported = reportedTypes.has(name); - - if (isExternalized || isReported) { - return; - } - - context.report({ - node: typeNode, - messageId: 'requireTypeExport', - data: { - name, - }, - }); - - reportedTypes.add(name); + checkTypeNode(typeNode); } } } @@ -142,40 +119,42 @@ export default createRule<[], MessageIds>({ return; } - const typeNodes = getReturnTypeTypesNodes(returnType).flatMap( - typeNode => { - return convertGenericTypeToTypeReferences(node, typeNode); - }, - ); + const typeNodes = getReturnTypeTypeNodes(returnType).flatMap(typeNode => { + return convertGenericTypeToTypeReferences(node, typeNode); + }); for (const typeNode of typeNodes) { - const name = getTypeName(typeNode); - - if (!name) { - // TODO: Report the whole function? Is this case even possible? - return; - } + checkTypeNode(typeNode); + } + } - const isExternalized = externalizedTypes.has(name); - const isReported = reportedTypes.has(name); + function checkTypeNode(node: TSESTree.TSTypeReference): void { + const name = getTypeName(node); - if (isExternalized || isReported) { - return; - } + if (!name) { + // TODO: Report the whole function? Is this case even possible? + return; + } - context.report({ - node: typeNode, - messageId: 'requireTypeExport', - data: { - name, - }, - }); + const isExternalized = externalizedTypes.has(name); + const isReported = reportedTypes.has(name); - reportedTypes.add(name); + if (isExternalized || isReported) { + return; } + + context.report({ + node: node, + messageId: 'requireTypeExport', + data: { + name, + }, + }); + + reportedTypes.add(name); } - function getParamTypesNodes( + function getParamTypeNodes( param: TSESTree.Parameter, ): TSESTree.TSTypeReference[] { // Single type @@ -254,7 +233,7 @@ export default createRule<[], MessageIds>({ return []; } - function getReturnTypeTypesNodes( + function getReturnTypeTypeNodes( typeAnnotation: TSESTree.TSTypeAnnotation, ): TSESTree.TSTypeReference[] { // Single type @@ -318,50 +297,49 @@ export default createRule<[], MessageIds>({ return typeNode; } - for (const definition of variable.defs) { + for (const def of variable.defs) { if ( - definition.type === DefinitionType.Type && - definition.node.type === AST_NODE_TYPES.TSTypeParameter && - definition.node.constraint + def.type !== DefinitionType.Type || + def.node.type !== AST_NODE_TYPES.TSTypeParameter || + !def.node.constraint ) { - switch (definition.node.constraint.type) { - // T extends SomeType - case AST_NODE_TYPES.TSTypeReference: - return definition.node.constraint; - - // T extends SomeType | AnotherType - // T extends SomeType & AnotherType - case AST_NODE_TYPES.TSUnionType: - case AST_NODE_TYPES.TSIntersectionType: - return definition.node.constraint.types.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - - // T extends [SomeType, AnotherType] - case AST_NODE_TYPES.TSTupleType: - return definition.node.constraint.elementTypes.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - - // T extends { some: SomeType, another: AnotherType } - case AST_NODE_TYPES.TSTypeLiteral: - return definition.node.constraint.members.reduce< - TSESTree.TSTypeReference[] - >((acc, member) => { - if ( - member.type === AST_NODE_TYPES.TSPropertySignature && - member.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - acc.push(member.typeAnnotation.typeAnnotation); - } - - return acc; - }, []); - - default: - continue; - } + continue; + } + + switch (def.node.constraint.type) { + // T extends SomeType + case AST_NODE_TYPES.TSTypeReference: + return def.node.constraint; + + // T extends SomeType | AnotherType + // T extends SomeType & AnotherType + case AST_NODE_TYPES.TSUnionType: + case AST_NODE_TYPES.TSIntersectionType: + return def.node.constraint.types.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + + // T extends [SomeType, AnotherType] + case AST_NODE_TYPES.TSTupleType: + return def.node.constraint.elementTypes.filter( + type => type.type === AST_NODE_TYPES.TSTypeReference, + ) as TSESTree.TSTypeReference[]; + + // T extends { some: SomeType, another: AnotherType } + case AST_NODE_TYPES.TSTypeLiteral: + return def.node.constraint.members.reduce< + TSESTree.TSTypeReference[] + >((acc, member) => { + if ( + member.type === AST_NODE_TYPES.TSPropertySignature && + member.typeAnnotation?.typeAnnotation.type === + AST_NODE_TYPES.TSTypeReference + ) { + acc.push(member.typeAnnotation.typeAnnotation); + } + + return acc; + }, []); } } From 1812e37f6501844fc72fc1d4ae006300464a8e34 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 24 Apr 2024 12:21:57 +0300 Subject: [PATCH 17/66] fix nested types in functions --- .../src/rules/require-types-exports.ts | 240 +----------------- .../tests/rules/require-types-exports.test.ts | 48 ++++ 2 files changed, 62 insertions(+), 226 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 66347fb95569..b99ba0ea7102 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -50,8 +50,7 @@ export default createRule<[], MessageIds>({ declaration: TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction; }, ): void { - checkFunctionParamsTypes(node.declaration); - checkFunctionReturnType(node.declaration); + checkFunctionTypes(node.declaration); } function visitExportedVariableDeclaration( @@ -61,8 +60,7 @@ export default createRule<[], MessageIds>({ ): void { for (const declaration of node.declaration.declarations) { if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { - checkFunctionParamsTypes(declaration.init); - checkFunctionReturnType(declaration.init); + checkFunctionTypes(declaration.init); } } } @@ -72,8 +70,7 @@ export default createRule<[], MessageIds>({ declaration: TSESTree.ArrowFunctionExpression; }, ): void { - checkFunctionParamsTypes(node.declaration); - checkFunctionReturnType(node.declaration); + checkFunctionTypes(node.declaration); } function visitDefaultExportedIdentifier( @@ -94,38 +91,21 @@ export default createRule<[], MessageIds>({ (def.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression || def.node.init?.type === AST_NODE_TYPES.FunctionExpression) ) { - checkFunctionParamsTypes(def.node.init); - checkFunctionReturnType(def.node.init); + checkFunctionTypes(def.node.init); } } } - function checkFunctionParamsTypes(node: FunctionNode): void { - for (const param of node.params) { - const typeNodes = getParamTypeNodes(param).flatMap(typeNode => { - return convertGenericTypeToTypeReferences(node, typeNode); - }); - - for (const typeNode of typeNodes) { - checkTypeNode(typeNode); - } - } - } - - function checkFunctionReturnType(node: FunctionNode): void { - const { returnType } = node; - - if (!returnType) { - return; - } - - const typeNodes = getReturnTypeTypeNodes(returnType).flatMap(typeNode => { - return convertGenericTypeToTypeReferences(node, typeNode); - }); + function checkFunctionTypes(node: FunctionNode): void { + const scope = context.sourceCode.getScope(node); - for (const typeNode of typeNodes) { - checkTypeNode(typeNode); - } + scope.through + .map(ref => ref.identifier.parent) + .filter( + (node): node is TSESTree.TSTypeReference => + node.type === AST_NODE_TYPES.TSTypeReference, + ) + .forEach(checkTypeNode); } function checkTypeNode(node: TSESTree.TSTypeReference): void { @@ -154,198 +134,6 @@ export default createRule<[], MessageIds>({ reportedTypes.add(name); } - function getParamTypeNodes( - param: TSESTree.Parameter, - ): TSESTree.TSTypeReference[] { - // Single type - if ( - param.type === AST_NODE_TYPES.Identifier && - param.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - return [param.typeAnnotation.typeAnnotation]; - } - - // Union or intersection - if ( - param.type === AST_NODE_TYPES.Identifier && - (param.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSUnionType || - param.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSIntersectionType) - ) { - return param.typeAnnotation.typeAnnotation.types.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - } - - // Tuple - if ( - param.type === AST_NODE_TYPES.ArrayPattern && - param.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSTupleType - ) { - return param.typeAnnotation.typeAnnotation.elementTypes.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - } - - // Inline object - if ( - param.type === AST_NODE_TYPES.ObjectPattern && - param.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeLiteral - ) { - return param.typeAnnotation.typeAnnotation.members.reduce< - TSESTree.TSTypeReference[] - >((acc, member) => { - if ( - member.type === AST_NODE_TYPES.TSPropertySignature && - member.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - acc.push(member.typeAnnotation.typeAnnotation); - } - - return acc; - }, []); - } - - // Rest params - if ( - param.type === AST_NODE_TYPES.RestElement && - param.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSArrayType && - param.typeAnnotation.typeAnnotation.elementType.type === - AST_NODE_TYPES.TSTypeReference - ) { - return [param.typeAnnotation.typeAnnotation.elementType]; - } - - // Default value assignment - if ( - param.type === AST_NODE_TYPES.AssignmentPattern && - param.left.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - return [param.left.typeAnnotation.typeAnnotation]; - } - - return []; - } - - function getReturnTypeTypeNodes( - typeAnnotation: TSESTree.TSTypeAnnotation, - ): TSESTree.TSTypeReference[] { - // Single type - if ( - typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference - ) { - return [typeAnnotation.typeAnnotation]; - } - - // Union or intersection - if ( - typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSUnionType || - typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSIntersectionType - ) { - return typeAnnotation.typeAnnotation.types.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - } - - // Tuple - if (typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTupleType) { - return typeAnnotation.typeAnnotation.elementTypes.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - } - - // Inline object - if (typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeLiteral) { - return typeAnnotation.typeAnnotation.members.reduce< - TSESTree.TSTypeReference[] - >((acc, member) => { - if ( - member.type === AST_NODE_TYPES.TSPropertySignature && - member.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - acc.push(member.typeAnnotation.typeAnnotation); - } - - return acc; - }, []); - } - - return []; - } - - function convertGenericTypeToTypeReferences( - functionNode: FunctionNode, - typeNode: TSESTree.TSTypeReference, - ): TSESTree.TSTypeReference | TSESTree.TSTypeReference[] { - const typeName = getTypeName(typeNode); - - if (!typeName) { - return typeNode; - } - - const scope = context.sourceCode.getScope(functionNode); - const variable = scope.set.get(typeName); - - if (!variable?.isTypeVariable) { - return typeNode; - } - - for (const def of variable.defs) { - if ( - def.type !== DefinitionType.Type || - def.node.type !== AST_NODE_TYPES.TSTypeParameter || - !def.node.constraint - ) { - continue; - } - - switch (def.node.constraint.type) { - // T extends SomeType - case AST_NODE_TYPES.TSTypeReference: - return def.node.constraint; - - // T extends SomeType | AnotherType - // T extends SomeType & AnotherType - case AST_NODE_TYPES.TSUnionType: - case AST_NODE_TYPES.TSIntersectionType: - return def.node.constraint.types.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - - // T extends [SomeType, AnotherType] - case AST_NODE_TYPES.TSTupleType: - return def.node.constraint.elementTypes.filter( - type => type.type === AST_NODE_TYPES.TSTypeReference, - ) as TSESTree.TSTypeReference[]; - - // T extends { some: SomeType, another: AnotherType } - case AST_NODE_TYPES.TSTypeLiteral: - return def.node.constraint.members.reduce< - TSESTree.TSTypeReference[] - >((acc, member) => { - if ( - member.type === AST_NODE_TYPES.TSPropertySignature && - member.typeAnnotation?.typeAnnotation.type === - AST_NODE_TYPES.TSTypeReference - ) { - acc.push(member.typeAnnotation.typeAnnotation); - } - - return acc; - }, []); - } - } - - return typeNode; - } - function getTypeName(typeReference: TSESTree.TSTypeReference): string { if (typeReference.typeName.type === AST_NODE_TYPES.Identifier) { return typeReference.typeName.name; @@ -355,7 +143,7 @@ export default createRule<[], MessageIds>({ } return { - 'ImportDeclaration[importKind="type"] ImportSpecifier, ImportSpecifier[importKind="type"]': + 'ImportDeclaration ImportSpecifier, ImportSpecifier': collectImportedTypes, 'ExportNamedDeclaration TSTypeAliasDeclaration, ExportNamedDeclaration TSInterfaceDeclaration': diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index c83b3b893413..6d05d62a244b 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -268,6 +268,12 @@ ruleTester.run('require-types-exports', rule, { export function f(a: Arg): void {} `, + ` + import { Arg } from './types'; + + export function f(a: Arg): void {} + `, + ` import type { Arg } from './types'; @@ -1594,6 +1600,48 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type Arg1 = number; + type Arg2 = boolean; + type Ret = string; + + export declare function f( + a: { b: { c: Arg1 | number | { d: T } } }, + e: Arg1, + ): { a: { b: T | Ret } }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 45, + endColumn: 49, + data: { + name: 'Arg2', + }, + }, + { + messageId: 'requireTypeExport', + line: 7, + column: 24, + endColumn: 28, + data: { + name: 'Arg1', + }, + }, + { + messageId: 'requireTypeExport', + line: 9, + column: 26, + endColumn: 29, + data: { + name: 'Ret', + }, + }, + ], + }, + { code: ` type Arg1 = number; From 4bee779c4082e68cad2713f5e85663e95c5d62b2 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 24 Apr 2024 12:29:15 +0300 Subject: [PATCH 18/66] fix docs --- .../docs/rules/require-types-exports.md | 13 +++++++------ .../src/rules/require-types-exports.ts | 3 +-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/require-types-exports.md b/packages/eslint-plugin/docs/rules/require-types-exports.md index 767050deeba0..9343536e6d74 100644 --- a/packages/eslint-plugin/docs/rules/require-types-exports.md +++ b/packages/eslint-plugin/docs/rules/require-types-exports.md @@ -1,16 +1,17 @@ --- -description: 'Require exporting types that are used in exported functions declarations.' +description: 'Require exporting types that are used in exported entities.' --- > 🛑 This file is source code, not the primary documentation location! 🛑 > > See **https://typescript-eslint.io/rules/require-types-exports** for documentation. -When exporting functions from a module, it is recommended to export also all the -types that are used in the function declarations. This is useful for consumers of -the module, as it allows them to use the types in their own code without having to -use things like [`Parameters`](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype) -or [`ReturnType`](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) to extract the types from the function. +When exporting entities from a module, it is recommended to export also all the +types that are used in their declarations. This is useful for consumers of the +module, as it allows them to use the types in their own code without having to +use utility types like [`Parameters`](https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype) +or [`ReturnType`](https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) +in order to extract the types from your code. ## Examples diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index b99ba0ea7102..e5cde3687c4d 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -19,8 +19,7 @@ export default createRule<[], MessageIds>({ docs: { recommended: 'strict', requiresTypeChecking: true, - description: - 'Require exporting types that are used in exported functions declarations', + description: 'Require exporting types that are used in exported entities', }, messages: { requireTypeExport: 'Expected type "{{ name }}" to be exported', From 26e7be7153ebb95c8f69040a9132f82472192ed2 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 24 Apr 2024 12:47:42 +0300 Subject: [PATCH 19/66] add inferred return type test case --- .../tests/rules/require-types-exports.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 6d05d62a244b..63d36fd3fd16 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -291,6 +291,18 @@ ruleTester.run('require-types-exports', rule, { export function f(a: T): void {} `, + + ` + export type R = number; + + export function f() { + const value: { num: R } = { + num: 1, + }; + + return value; + } + `, ], invalid: [ @@ -1600,6 +1612,31 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type R = number; + + export function f() { + const value: { num: R } = { + num: 1, + }; + + return value; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 31, + endColumn: 32, + data: { + name: 'R', + }, + }, + ], + }, + { code: ` type Arg1 = number; From e57985a6fdbd710c8deffd8ad34b52054e3b8422 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 24 Apr 2024 16:21:43 +0300 Subject: [PATCH 20/66] stupidly check for variable types --- .../src/rules/require-types-exports.ts | 68 +++++++++++++++++++ .../tests/rules/require-types-exports.test.ts | 58 ++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index e5cde3687c4d..3a803a25f8a9 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,3 +1,4 @@ +import type { Reference } from '@typescript-eslint/scope-manager'; import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -12,6 +13,12 @@ type FunctionNode = | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression; +type TypeReference = Reference & { + identifier: { + parent: TSESTree.TSTypeReference; + }; +}; + export default createRule<[], MessageIds>({ name: 'require-types-exports', meta: { @@ -28,9 +35,23 @@ export default createRule<[], MessageIds>({ }, defaultOptions: [], create(context) { + const typeReferences = new Set(); const externalizedTypes = new Set(); const reportedTypes = new Set(); + function collectTypeReferences(node: TSESTree.Program): void { + const scope = context.sourceCode.getScope(node); + + scope.references.forEach(r => { + if ( + r.resolved?.isTypeVariable && + r.identifier.parent.type === AST_NODE_TYPES.TSTypeReference + ) { + typeReferences.add(r as TypeReference); + } + }); + } + function collectImportedTypes(node: TSESTree.ImportSpecifier): void { externalizedTypes.add(node.local.name); } @@ -60,6 +81,8 @@ export default createRule<[], MessageIds>({ for (const declaration of node.declaration.declarations) { if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { checkFunctionTypes(declaration.init); + } else { + checkVariableTypes(declaration); } } } @@ -107,6 +130,21 @@ export default createRule<[], MessageIds>({ .forEach(checkTypeNode); } + function checkVariableTypes( + node: TSESTree.LetOrConstOrVarDeclarator, + ): void { + if (node.id.type !== AST_NODE_TYPES.Identifier) { + return; + } + + typeReferences.forEach(r => { + // TODO: Probably not the best way to do it... + if (isLocationOverlapping(r.identifier.loc, node.loc)) { + checkTypeNode(r.identifier.parent); + } + }); + } + function checkTypeNode(node: TSESTree.TSTypeReference): void { const name = getTypeName(node); @@ -141,7 +179,37 @@ export default createRule<[], MessageIds>({ return ''; } + function isLocationOverlapping( + location: TSESTree.Node['loc'], + container: TSESTree.Node['loc'], + ): boolean { + if ( + location.start.line < container.start.line || + location.end.line > container.end.line + ) { + return false; + } + + if ( + location.start.line === container.start.line && + location.start.column < container.start.column + ) { + return false; + } + + if ( + location.end.line === container.end.line && + location.end.column > container.end.column + ) { + return false; + } + + return true; + } + return { + Program: collectTypeReferences, + 'ImportDeclaration ImportSpecifier, ImportSpecifier': collectImportedTypes, diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 63d36fd3fd16..39f66ebf2359 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -303,6 +303,24 @@ ruleTester.run('require-types-exports', rule, { return value; } `, + + ` + import type { A } from './types'; + + export type T1 = number; + + export interface T2 { + key: number; + } + + export const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + `, ], invalid: [ @@ -1739,5 +1757,45 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + import type { A } from './types'; + + type T1 = number; + + interface T2 { + key: number; + } + + export const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 10, + column: 44, + endColumn: 46, + data: { + name: 'T1', + }, + }, + { + messageId: 'requireTypeExport', + line: 10, + column: 64, + endColumn: 66, + data: { + name: 'T2', + }, + }, + ], + }, ], }); From cbb784c09a1aef1864d76c2cce72d2a0140d4fb6 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 24 Apr 2024 18:09:19 +0300 Subject: [PATCH 21/66] support default exported variable --- .../src/rules/require-types-exports.ts | 15 ++--- .../tests/rules/require-types-exports.test.ts | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 3a803a25f8a9..62b64785dfb9 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -108,12 +108,17 @@ export default createRule<[], MessageIds>({ } for (const def of variable.defs) { + if (def.type !== DefinitionType.Variable || !def.node.init) { + continue; + } + if ( - def.type === DefinitionType.Variable && - (def.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression || - def.node.init?.type === AST_NODE_TYPES.FunctionExpression) + def.node.init.type === AST_NODE_TYPES.ArrowFunctionExpression || + def.node.init.type === AST_NODE_TYPES.FunctionExpression ) { checkFunctionTypes(def.node.init); + } else { + checkVariableTypes(def.node); } } } @@ -133,10 +138,6 @@ export default createRule<[], MessageIds>({ function checkVariableTypes( node: TSESTree.LetOrConstOrVarDeclarator, ): void { - if (node.id.type !== AST_NODE_TYPES.Identifier) { - return; - } - typeReferences.forEach(r => { // TODO: Probably not the best way to do it... if (isLocationOverlapping(r.identifier.loc, node.loc)) { diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 39f66ebf2359..857cc404b714 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -321,6 +321,26 @@ ruleTester.run('require-types-exports', rule, { }, }; `, + + ` + import type { A } from './types'; + + export type T1 = number; + + export interface T2 { + key: number; + } + + const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + + export default value; + `, ], invalid: [ @@ -1797,5 +1817,47 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + import type { A } from './types'; + + type T1 = number; + + interface T2 { + key: number; + } + + const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + + export default value; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 10, + column: 37, + endColumn: 39, + data: { + name: 'T1', + }, + }, + { + messageId: 'requireTypeExport', + line: 10, + column: 57, + endColumn: 59, + data: { + name: 'T2', + }, + }, + ], + }, ], }); From 6fb274af938403379ede6305350c5827ddedc879 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 19 May 2024 21:43:16 +0300 Subject: [PATCH 22/66] update docs --- ...-types-exports.md => require-types-exports.mdx} | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) rename packages/eslint-plugin/docs/rules/{require-types-exports.md => require-types-exports.mdx} (90%) diff --git a/packages/eslint-plugin/docs/rules/require-types-exports.md b/packages/eslint-plugin/docs/rules/require-types-exports.mdx similarity index 90% rename from packages/eslint-plugin/docs/rules/require-types-exports.md rename to packages/eslint-plugin/docs/rules/require-types-exports.mdx index 9343536e6d74..baa939fc2115 100644 --- a/packages/eslint-plugin/docs/rules/require-types-exports.md +++ b/packages/eslint-plugin/docs/rules/require-types-exports.mdx @@ -2,6 +2,9 @@ description: 'Require exporting types that are used in exported entities.' --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + > 🛑 This file is source code, not the primary documentation location! 🛑 > > See **https://typescript-eslint.io/rules/require-types-exports** for documentation. @@ -15,9 +18,8 @@ in order to extract the types from your code. ## Examples - - -### ❌ Incorrect + + ```ts type Arg = string; @@ -43,7 +45,8 @@ enum Color { export declare function getRandomColor(): Color; ``` -### ✅ Correct + + ```ts export type Arg = string; @@ -69,7 +72,8 @@ export enum Color { export declare function getRandomColor(): Color; ``` - + + ## When Not To Use It From 4672fe17f5137e35d597d874e420cd39e7b424f3 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 19 May 2024 21:56:38 +0300 Subject: [PATCH 23/66] wip --- .../src/rules/require-types-exports.ts | 9 ++- .../require-types-exports.shot | 59 +++++++++++++++++++ .../tests/rules/require-types-exports.test.ts | 10 ++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/require-types-exports.shot diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 62b64785dfb9..bb7646656239 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -24,9 +24,9 @@ export default createRule<[], MessageIds>({ meta: { type: 'suggestion', docs: { + description: 'Require exporting types that are used in exported entities', recommended: 'strict', requiresTypeChecking: true, - description: 'Require exporting types that are used in exported entities', }, messages: { requireTypeExport: 'Expected type "{{ name }}" to be exported', @@ -57,7 +57,10 @@ export default createRule<[], MessageIds>({ } function collectExportedTypes( - node: TSESTree.TSTypeAliasDeclaration | TSESTree.TSInterfaceDeclaration, + node: + | TSESTree.TSTypeAliasDeclaration + | TSESTree.TSInterfaceDeclaration + | TSESTree.TSEnumDeclaration, ): void { externalizedTypes.add(node.id.name); } @@ -214,7 +217,7 @@ export default createRule<[], MessageIds>({ 'ImportDeclaration ImportSpecifier, ImportSpecifier': collectImportedTypes, - 'ExportNamedDeclaration TSTypeAliasDeclaration, ExportNamedDeclaration TSInterfaceDeclaration': + 'ExportNamedDeclaration TSTypeAliasDeclaration, ExportNamedDeclaration TSInterfaceDeclaration, ExportNamedDeclaration TSEnumDeclaration': collectExportedTypes, 'ExportNamedDeclaration[declaration.type="FunctionDeclaration"]': diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/require-types-exports.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/require-types-exports.shot new file mode 100644 index 000000000000..f515f012d362 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/require-types-exports.shot @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs require-types-exports.mdx code examples ESLint output 1`] = ` +"Incorrect + +type Arg = string; +type Result = number; + +export function strLength(arg: Arg): Result { + ~~~ Expected type "Arg" to be exported + ~~~~~~ Expected type "Result" to be exported + return arg.length; +} + +interface Fruit { + name: string; + color: string; +} + +export const getFruitName = (fruit: Fruit) => fruit.name; + ~~~~~ Expected type "Fruit" to be exported + +enum Color { + Red = 'red', + Green = 'green', + Blue = 'blue', +} + +export declare function getRandomColor(): Color; + ~~~~~ Expected type "Color" to be exported +" +`; + +exports[`Validating rule docs require-types-exports.mdx code examples ESLint output 2`] = ` +"Correct + +export type Arg = string; +export type Result = number; + +export function strLength(arg: Arg): Result { + return arg.length; +} + +export interface Fruit { + name: string; + color: string; +} + +export const getFruitName = (fruit: Fruit) => fruit.name; + +export enum Color { + Red = 'red', + Green = 'green', + Blue = 'blue', +} + +export declare function getRandomColor(): Color; +" +`; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 857cc404b714..b2e8c2a35790 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -341,6 +341,16 @@ ruleTester.run('require-types-exports', rule, { export default value; `, + + ` + export enum Fruit { + Apple, + Banana, + Cherry, + } + + export function f(a: Fruit): void {} + `, ], invalid: [ From c79b5cbaa0d25d4164708eca04a61082b4785312 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 19 May 2024 22:09:44 +0300 Subject: [PATCH 24/66] wip --- packages/eslint-plugin/src/configs/strict-type-checked-only.ts | 1 + packages/typescript-eslint/src/configs/all.ts | 1 + packages/typescript-eslint/src/configs/disable-type-checked.ts | 1 + .../typescript-eslint/src/configs/strict-type-checked-only.ts | 1 + packages/typescript-eslint/src/configs/strict-type-checked.ts | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 12709933dfb7..d5b62fb4fb70 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -43,6 +43,7 @@ export = { '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': [ 'error', { diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index efe0d16fceb8..0579a6d7d89b 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -153,6 +153,7 @@ export default ( '@typescript-eslint/require-array-sort-compare': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': 'error', '@typescript-eslint/restrict-template-expressions': 'error', 'no-return-await': 'off', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 8d2f29220aba..2c789c0df718 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -60,6 +60,7 @@ export default ( '@typescript-eslint/promise-function-async': 'off', '@typescript-eslint/require-array-sort-compare': 'off', '@typescript-eslint/require-await': 'off', + '@typescript-eslint/require-types-exports': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index f17b5280ca49..765371534b6a 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -52,6 +52,7 @@ export default ( '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': [ 'error', { diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index ad62ee749e25..ddb7afd1db94 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -83,6 +83,7 @@ export default ( '@typescript-eslint/prefer-ts-expect-error': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': [ 'error', { From 279055a127fbfd01054d863f08dcf49cee4d5e34 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 19 May 2024 22:10:47 +0300 Subject: [PATCH 25/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index bb7646656239..7b7d3924d303 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -26,7 +26,6 @@ export default createRule<[], MessageIds>({ docs: { description: 'Require exporting types that are used in exported entities', recommended: 'strict', - requiresTypeChecking: true, }, messages: { requireTypeExport: 'Expected type "{{ name }}" to be exported', From 2f81933fb23c0c6d937db7b1ebe9c39fc6690d56 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 2 Jun 2024 22:14:30 +0300 Subject: [PATCH 26/66] improve types --- packages/eslint-plugin/src/rules/require-types-exports.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 7b7d3924d303..9236274ca627 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -14,6 +14,7 @@ type FunctionNode = | TSESTree.FunctionExpression; type TypeReference = Reference & { + isTypeReference: true; identifier: { parent: TSESTree.TSTypeReference; }; @@ -44,7 +45,9 @@ export default createRule<[], MessageIds>({ scope.references.forEach(r => { if ( r.resolved?.isTypeVariable && - r.identifier.parent.type === AST_NODE_TYPES.TSTypeReference + r.identifier.type === AST_NODE_TYPES.Identifier && + r.identifier.parent.type === AST_NODE_TYPES.TSTypeReference && + r.isTypeReference ) { typeReferences.add(r as TypeReference); } From 0f788d2e38afb2c3f6f704c7ac0df8d693e6548e Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 2 Jun 2024 22:15:18 +0300 Subject: [PATCH 27/66] improve type reference search --- .../src/rules/require-types-exports.ts | 48 ++++++---------- .../tests/rules/require-types-exports.test.ts | 57 +++++++++++++++++++ 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 9236274ca627..abdad788019c 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -144,13 +144,29 @@ export default createRule<[], MessageIds>({ node: TSESTree.LetOrConstOrVarDeclarator, ): void { typeReferences.forEach(r => { - // TODO: Probably not the best way to do it... - if (isLocationOverlapping(r.identifier.loc, node.loc)) { + if (isAncestorNode(node, r.identifier.parent)) { checkTypeNode(r.identifier.parent); } }); } + function isAncestorNode( + ancestor: TSESTree.Node, + node: TSESTree.Node, + ): boolean { + let parent = node.parent; + + while (parent) { + if (parent === ancestor) { + return true; + } + + parent = parent.parent; + } + + return false; + } + function checkTypeNode(node: TSESTree.TSTypeReference): void { const name = getTypeName(node); @@ -185,34 +201,6 @@ export default createRule<[], MessageIds>({ return ''; } - function isLocationOverlapping( - location: TSESTree.Node['loc'], - container: TSESTree.Node['loc'], - ): boolean { - if ( - location.start.line < container.start.line || - location.end.line > container.end.line - ) { - return false; - } - - if ( - location.start.line === container.start.line && - location.start.column < container.start.column - ) { - return false; - } - - if ( - location.end.line === container.end.line && - location.end.column > container.end.column - ) { - return false; - } - - return true; - } - return { Program: collectTypeReferences, diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index b2e8c2a35790..ec41d6d2e2a2 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -1869,5 +1869,62 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type T1 = number; + + interface T2 { + key: number; + } + + type T3 = boolean; + + export const value: + | { + a: T1; + b: { + c: T2; + }; + } + | T3[] = { + a: 1, + b: { + c: { + key: 1, + }, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 12, + column: 18, + endColumn: 20, + data: { + name: 'T1', + }, + }, + { + messageId: 'requireTypeExport', + line: 14, + column: 20, + endColumn: 22, + data: { + name: 'T2', + }, + }, + { + messageId: 'requireTypeExport', + line: 17, + column: 13, + endColumn: 15, + data: { + name: 'T3', + }, + }, + ], + }, ], }); From 6cec0f56a80fdc10e57d240ac6cfd517efdec4ce Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 2 Jun 2024 22:42:13 +0300 Subject: [PATCH 28/66] don't report types from default library --- .../src/rules/require-types-exports.ts | 15 ++++++++++++++- .../tests/rules/require-types-exports.test.ts | 12 ++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index abdad788019c..a5af6ede8b53 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -3,7 +3,11 @@ import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule } from '../util'; +import { + createRule, + getParserServices, + isSymbolFromDefaultLibrary, +} from '../util'; type MessageIds = 'requireTypeExport'; @@ -35,6 +39,8 @@ export default createRule<[], MessageIds>({ }, defaultOptions: [], create(context) { + const services = getParserServices(context); + const typeReferences = new Set(); const externalizedTypes = new Set(); const reportedTypes = new Set(); @@ -182,6 +188,13 @@ export default createRule<[], MessageIds>({ return; } + const tsNode = services.esTreeNodeToTSNodeMap.get(node); + const type = services.program.getTypeChecker().getTypeAtLocation(tsNode); + + if (isSymbolFromDefaultLibrary(services.program, type.getSymbol())) { + return; + } + context.report({ node: node, messageId: 'requireTypeExport', diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index ec41d6d2e2a2..c02324e22abd 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -351,6 +351,18 @@ ruleTester.run('require-types-exports', rule, { export function f(a: Fruit): void {} `, + + ` + export function f(arg: Record>) { + return arg; + } + `, + + ` + export function f>>(arg: T) { + return arg; + } + `, ], invalid: [ From 497957a7f9df3a96285ea5a5c09231738a58cee6 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 2 Jun 2024 23:21:47 +0300 Subject: [PATCH 29/66] getTypeName --- .../src/rules/require-types-exports.ts | 22 +++++++---- .../tests/rules/require-types-exports.test.ts | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index a5af6ede8b53..00a68f732ef5 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -174,10 +174,11 @@ export default createRule<[], MessageIds>({ } function checkTypeNode(node: TSESTree.TSTypeReference): void { - const name = getTypeName(node); + const name = getTypeName(node.typeName); - if (!name) { - // TODO: Report the whole function? Is this case even possible? + // Using `this` type is allowed since it's necessarily exported + // if it's used in an exported entity. + if (name === 'this') { return; } @@ -206,12 +207,17 @@ export default createRule<[], MessageIds>({ reportedTypes.add(name); } - function getTypeName(typeReference: TSESTree.TSTypeReference): string { - if (typeReference.typeName.type === AST_NODE_TYPES.Identifier) { - return typeReference.typeName.name; - } + function getTypeName(typeName: TSESTree.EntityName): string { + switch (typeName.type) { + case AST_NODE_TYPES.Identifier: + return typeName.name; + + case AST_NODE_TYPES.TSQualifiedName: + return getTypeName(typeName.left) + '.' + typeName.right.name; - return ''; + case AST_NODE_TYPES.ThisExpression: + return 'this'; + } } return { diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index c02324e22abd..d7b597a4d9d6 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -363,6 +363,22 @@ ruleTester.run('require-types-exports', rule, { return arg; } `, + + ` + export class Wrapper { + work(other: this) {} + } + `, + + ` + export namespace A { + export type B = number; + } + + export function a(arg: A.B) { + return arg; + } + `, ], invalid: [ @@ -1800,6 +1816,29 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + export namespace A { + type B = number; + } + + export function a(arg: A.B) { + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 32, + endColumn: 35, + data: { + name: 'A.B', + }, + }, + ], + }, + { code: ` import type { A } from './types'; From 700ff85cf3c74fd2cc3547328ea3e4af14d085e1 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 4 Jun 2024 21:07:27 +0300 Subject: [PATCH 30/66] move utils out of the closure --- .../src/rules/require-types-exports.ts | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 00a68f732ef5..02b2a224cd76 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -156,23 +156,6 @@ export default createRule<[], MessageIds>({ }); } - function isAncestorNode( - ancestor: TSESTree.Node, - node: TSESTree.Node, - ): boolean { - let parent = node.parent; - - while (parent) { - if (parent === ancestor) { - return true; - } - - parent = parent.parent; - } - - return false; - } - function checkTypeNode(node: TSESTree.TSTypeReference): void { const name = getTypeName(node.typeName); @@ -207,19 +190,6 @@ export default createRule<[], MessageIds>({ reportedTypes.add(name); } - function getTypeName(typeName: TSESTree.EntityName): string { - switch (typeName.type) { - case AST_NODE_TYPES.Identifier: - return typeName.name; - - case AST_NODE_TYPES.TSQualifiedName: - return getTypeName(typeName.left) + '.' + typeName.right.name; - - case AST_NODE_TYPES.ThisExpression: - return 'this'; - } - } - return { Program: collectTypeReferences, @@ -249,3 +219,30 @@ export default createRule<[], MessageIds>({ }; }, }); + +function getTypeName(typeName: TSESTree.EntityName): string { + switch (typeName.type) { + case AST_NODE_TYPES.Identifier: + return typeName.name; + + case AST_NODE_TYPES.TSQualifiedName: + return getTypeName(typeName.left) + '.' + typeName.right.name; + + case AST_NODE_TYPES.ThisExpression: + return 'this'; + } +} + +function isAncestorNode(ancestor: TSESTree.Node, node: TSESTree.Node): boolean { + let parent = node.parent; + + while (parent) { + if (parent === ancestor) { + return true; + } + + parent = parent.parent; + } + + return false; +} From 9a155b3058d37bc76fd41afb4c7e8d97aab471f9 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 21:32:33 +0300 Subject: [PATCH 31/66] support namespaced types --- .../src/rules/require-types-exports.ts | 51 +++++++++++++++---- .../tests/rules/require-types-exports.test.ts | 20 +++++--- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 02b2a224cd76..fa5565d4533a 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -137,13 +137,13 @@ export default createRule<[], MessageIds>({ function checkFunctionTypes(node: FunctionNode): void { const scope = context.sourceCode.getScope(node); - scope.through - .map(ref => ref.identifier.parent) - .filter( - (node): node is TSESTree.TSTypeReference => - node.type === AST_NODE_TYPES.TSTypeReference, - ) - .forEach(checkTypeNode); + scope.through.forEach(ref => { + const typeRef = findClosestTypeReference(ref.identifier, node); + + if (typeRef) { + checkTypeNode(typeRef); + } + }); } function checkVariableTypes( @@ -157,7 +157,7 @@ export default createRule<[], MessageIds>({ } function checkTypeNode(node: TSESTree.TSTypeReference): void { - const name = getTypeName(node.typeName); + let name = getTypeName(node.typeName); // Using `this` type is allowed since it's necessarily exported // if it's used in an exported entity. @@ -165,6 +165,12 @@ export default createRule<[], MessageIds>({ return; } + // Namespaced types are not exported directly, so we check the + // leftmost part of the name. + if (Array.isArray(name)) { + name = name[0]; + } + const isExternalized = externalizedTypes.has(name); const isReported = reportedTypes.has(name); @@ -196,7 +202,13 @@ export default createRule<[], MessageIds>({ 'ImportDeclaration ImportSpecifier, ImportSpecifier': collectImportedTypes, - 'ExportNamedDeclaration TSTypeAliasDeclaration, ExportNamedDeclaration TSInterfaceDeclaration, ExportNamedDeclaration TSEnumDeclaration': + 'Program > ExportNamedDeclaration > TSTypeAliasDeclaration': + collectExportedTypes, + 'Program > ExportNamedDeclaration > TSInterfaceDeclaration': + collectExportedTypes, + 'Program > ExportNamedDeclaration > TSEnumDeclaration': + collectExportedTypes, + 'Program > ExportNamedDeclaration > TSModuleDeclaration': collectExportedTypes, 'ExportNamedDeclaration[declaration.type="FunctionDeclaration"]': @@ -220,19 +232,36 @@ export default createRule<[], MessageIds>({ }, }); -function getTypeName(typeName: TSESTree.EntityName): string { +function getTypeName(typeName: TSESTree.EntityName): string | string[] { switch (typeName.type) { case AST_NODE_TYPES.Identifier: return typeName.name; case AST_NODE_TYPES.TSQualifiedName: - return getTypeName(typeName.left) + '.' + typeName.right.name; + return [...(getTypeName(typeName.left) || []), typeName.right.name]; case AST_NODE_TYPES.ThisExpression: return 'this'; } } +function findClosestTypeReference( + startNode: TSESTree.Node, + endNode: TSESTree.Node, +): TSESTree.TSTypeReference | null { + let parent = startNode.parent; + + while (parent && parent !== endNode) { + if (parent.type === AST_NODE_TYPES.TSTypeReference) { + return parent; + } + + parent = parent.parent; + } + + return null; +} + function isAncestorNode(ancestor: TSESTree.Node, node: TSESTree.Node): boolean { let parent = node.parent; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index d7b597a4d9d6..6a3008c3f29f 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -372,10 +372,12 @@ ruleTester.run('require-types-exports', rule, { ` export namespace A { - export type B = number; + export namespace B { + export type C = number; + } } - export function a(arg: A.B) { + export function a(arg: A.B.C) { return arg; } `, @@ -1818,22 +1820,24 @@ ruleTester.run('require-types-exports', rule, { { code: ` - export namespace A { - type B = number; + namespace A { + export namespace B { + export type C = number; + } } - export function a(arg: A.B) { + export function a(arg: A.B.C) { return arg; } `, errors: [ { messageId: 'requireTypeExport', - line: 6, + line: 8, column: 32, - endColumn: 35, + endColumn: 37, data: { - name: 'A.B', + name: 'A', }, }, ], From b65f9c474efadcbd304ca0757c60dedd5e249879 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 21:54:23 +0300 Subject: [PATCH 32/66] fix namespaced imports --- .../src/rules/require-types-exports.ts | 14 ++++++++++---- .../tests/rules/require-types-exports.test.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index fa5565d4533a..ae28c9d03711 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -199,8 +199,9 @@ export default createRule<[], MessageIds>({ return { Program: collectTypeReferences, - 'ImportDeclaration ImportSpecifier, ImportSpecifier': - collectImportedTypes, + 'ImportDeclaration ImportSpecifier': collectImportedTypes, + 'ImportDeclaration ImportNamespaceSpecifier': collectImportedTypes, + 'ImportDeclaration ImportDefaultSpecifier': collectImportedTypes, 'Program > ExportNamedDeclaration > TSTypeAliasDeclaration': collectExportedTypes, @@ -237,8 +238,13 @@ function getTypeName(typeName: TSESTree.EntityName): string | string[] { case AST_NODE_TYPES.Identifier: return typeName.name; - case AST_NODE_TYPES.TSQualifiedName: - return [...(getTypeName(typeName.left) || []), typeName.right.name]; + case AST_NODE_TYPES.TSQualifiedName: { + let left = getTypeName(typeName.left); + + left = Array.isArray(left) ? left : [left]; + + return [...left, typeName.right.name]; + } case AST_NODE_TYPES.ThisExpression: return 'this'; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 6a3008c3f29f..86da0ca52e91 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -381,6 +381,22 @@ ruleTester.run('require-types-exports', rule, { return arg; } `, + + ` + import * as ts from 'typescript'; + + export function a(arg: ts.Type) { + return arg; + } + `, + + ` + import ts from 'typescript'; + + export function a(arg: ts.Type) { + return arg; + } + `, ], invalid: [ From 078e24afcc6d8695367738fd163dfc8e20916605 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 22:16:57 +0300 Subject: [PATCH 33/66] WIP --- packages/eslint-plugin/src/rules/require-types-exports.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index ae28c9d03711..a812151985e6 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -60,7 +60,12 @@ export default createRule<[], MessageIds>({ }); } - function collectImportedTypes(node: TSESTree.ImportSpecifier): void { + function collectImportedTypes( + node: + | TSESTree.ImportSpecifier + | TSESTree.ImportNamespaceSpecifier + | TSESTree.ImportDefaultSpecifier, + ): void { externalizedTypes.add(node.local.name); } From ed23162a443c55cebcae9c716fd20e04973f1f98 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 22:45:56 +0300 Subject: [PATCH 34/66] wip --- .../src/rules/require-types-exports.ts | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index a812151985e6..a3def00e15ea 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -154,15 +154,15 @@ export default createRule<[], MessageIds>({ function checkVariableTypes( node: TSESTree.LetOrConstOrVarDeclarator, ): void { - typeReferences.forEach(r => { - if (isAncestorNode(node, r.identifier.parent)) { - checkTypeNode(r.identifier.parent); + typeReferences.forEach(ref => { + if (isAncestorNode(node, ref.identifier.parent)) { + checkTypeNode(ref.identifier.parent); } }); } function checkTypeNode(node: TSESTree.TSTypeReference): void { - let name = getTypeName(node.typeName); + const name = getTypeName(node.typeName); // Using `this` type is allowed since it's necessarily exported // if it's used in an exported entity. @@ -170,12 +170,6 @@ export default createRule<[], MessageIds>({ return; } - // Namespaced types are not exported directly, so we check the - // leftmost part of the name. - if (Array.isArray(name)) { - name = name[0]; - } - const isExternalized = externalizedTypes.has(name); const isReported = reportedTypes.has(name); @@ -238,18 +232,15 @@ export default createRule<[], MessageIds>({ }, }); -function getTypeName(typeName: TSESTree.EntityName): string | string[] { +function getTypeName(typeName: TSESTree.EntityName): string { switch (typeName.type) { case AST_NODE_TYPES.Identifier: return typeName.name; - case AST_NODE_TYPES.TSQualifiedName: { - let left = getTypeName(typeName.left); - - left = Array.isArray(left) ? left : [left]; - - return [...left, typeName.right.name]; - } + case AST_NODE_TYPES.TSQualifiedName: + // Namespaced types are not exported directly, so we check the + // leftmost part of the name. + return getTypeName(typeName.left); case AST_NODE_TYPES.ThisExpression: return 'this'; From ac224eb6107ba236204298b4a6e325bb9b61c7e5 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 22:46:07 +0300 Subject: [PATCH 35/66] fix propertykey tests --- packages/eslint-plugin/src/rules/require-types-exports.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index a3def00e15ea..420ca23f811e 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -179,8 +179,9 @@ export default createRule<[], MessageIds>({ const tsNode = services.esTreeNodeToTSNodeMap.get(node); const type = services.program.getTypeChecker().getTypeAtLocation(tsNode); + const symbol = type.aliasSymbol ?? type.getSymbol(); - if (isSymbolFromDefaultLibrary(services.program, type.getSymbol())) { + if (isSymbolFromDefaultLibrary(services.program, symbol)) { return; } From 417cc91ea799917a3c398805ecf16eef018cd765 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 5 Jun 2024 22:55:34 +0300 Subject: [PATCH 36/66] ReturnType test --- .../eslint-plugin/tests/rules/require-types-exports.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 86da0ca52e91..e58a6c55bc0b 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -364,6 +364,12 @@ ruleTester.run('require-types-exports', rule, { } `, + ` + export function f string>>(arg: T) { + return arg; + } + `, + ` export class Wrapper { work(other: this) {} From ae1b87c3474b4d31834f973fc07cbbeee055b295 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 28 Jun 2024 13:31:19 +0300 Subject: [PATCH 37/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 420ca23f811e..4ef1eed15f9d 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -155,7 +155,9 @@ export default createRule<[], MessageIds>({ node: TSESTree.LetOrConstOrVarDeclarator, ): void { typeReferences.forEach(ref => { - if (isAncestorNode(node, ref.identifier.parent)) { + const isTypeUsedInNode = isAncestorNode(node, ref.identifier.parent); + + if (isTypeUsedInNode) { checkTypeNode(ref.identifier.parent); } }); From d22740832b61459f6a9faccadb707cffe725d2ef Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 28 Jun 2024 18:09:12 +0300 Subject: [PATCH 38/66] collect type references recursively --- .../src/rules/require-types-exports.ts | 241 ++++--- .../tests/rules/require-types-exports.test.ts | 603 ++++++++++++++++++ 2 files changed, 743 insertions(+), 101 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 4ef1eed15f9d..44ccdee972da 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,6 +1,5 @@ -import type { Reference } from '@typescript-eslint/scope-manager'; import { DefinitionType } from '@typescript-eslint/scope-manager'; -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -11,19 +10,6 @@ import { type MessageIds = 'requireTypeExport'; -type FunctionNode = - | TSESTree.FunctionDeclaration - | TSESTree.TSDeclareFunction - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionExpression; - -type TypeReference = Reference & { - isTypeReference: true; - identifier: { - parent: TSESTree.TSTypeReference; - }; -}; - export default createRule<[], MessageIds>({ name: 'require-types-exports', meta: { @@ -41,25 +27,9 @@ export default createRule<[], MessageIds>({ create(context) { const services = getParserServices(context); - const typeReferences = new Set(); const externalizedTypes = new Set(); const reportedTypes = new Set(); - function collectTypeReferences(node: TSESTree.Program): void { - const scope = context.sourceCode.getScope(node); - - scope.references.forEach(r => { - if ( - r.resolved?.isTypeVariable && - r.identifier.type === AST_NODE_TYPES.Identifier && - r.identifier.parent.type === AST_NODE_TYPES.TSTypeReference && - r.isTypeReference - ) { - typeReferences.add(r as TypeReference); - } - }); - } - function collectImportedTypes( node: | TSESTree.ImportSpecifier @@ -82,11 +52,12 @@ export default createRule<[], MessageIds>({ node: ( | TSESTree.ExportNamedDeclaration | TSESTree.DefaultExportDeclarations + | TSESTree.ArrowFunctionExpression ) & { declaration: TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction; }, ): void { - checkFunctionTypes(node.declaration); + checkNodeTypes(node.declaration); } function visitExportedVariableDeclaration( @@ -95,22 +66,10 @@ export default createRule<[], MessageIds>({ }, ): void { for (const declaration of node.declaration.declarations) { - if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { - checkFunctionTypes(declaration.init); - } else { - checkVariableTypes(declaration); - } + checkNodeTypes(declaration); } } - function visitDefaultExportedArrowFunction( - node: TSESTree.ExportDefaultDeclaration & { - declaration: TSESTree.ArrowFunctionExpression; - }, - ): void { - checkFunctionTypes(node.declaration); - } - function visitDefaultExportedIdentifier( node: TSESTree.DefaultExportDeclarations & { declaration: TSESTree.Identifier; @@ -128,39 +87,17 @@ export default createRule<[], MessageIds>({ continue; } - if ( - def.node.init.type === AST_NODE_TYPES.ArrowFunctionExpression || - def.node.init.type === AST_NODE_TYPES.FunctionExpression - ) { - checkFunctionTypes(def.node.init); - } else { - checkVariableTypes(def.node); - } + checkNodeTypes(def.node); } } - function checkFunctionTypes(node: FunctionNode): void { - const scope = context.sourceCode.getScope(node); - - scope.through.forEach(ref => { - const typeRef = findClosestTypeReference(ref.identifier, node); + function checkNodeTypes(node: TSESTree.Node): void { + const typeReferences = getTypeReferencesRecursively( + node, + context.sourceCode, + ); - if (typeRef) { - checkTypeNode(typeRef); - } - }); - } - - function checkVariableTypes( - node: TSESTree.LetOrConstOrVarDeclarator, - ): void { - typeReferences.forEach(ref => { - const isTypeUsedInNode = isAncestorNode(node, ref.identifier.parent); - - if (isTypeUsedInNode) { - checkTypeNode(ref.identifier.parent); - } - }); + typeReferences.forEach(checkTypeNode); } function checkTypeNode(node: TSESTree.TSTypeReference): void { @@ -199,8 +136,6 @@ export default createRule<[], MessageIds>({ } return { - Program: collectTypeReferences, - 'ImportDeclaration ImportSpecifier': collectImportedTypes, 'ImportDeclaration ImportNamespaceSpecifier': collectImportedTypes, 'ImportDeclaration ImportDefaultSpecifier': collectImportedTypes, @@ -220,14 +155,14 @@ export default createRule<[], MessageIds>({ 'ExportNamedDeclaration[declaration.type="TSDeclareFunction"]': visitExportedFunctionDeclaration, - 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': - visitExportedVariableDeclaration, - 'ExportDefaultDeclaration[declaration.type="FunctionDeclaration"]': visitExportedFunctionDeclaration, 'ExportDefaultDeclaration[declaration.type="ArrowFunctionExpression"]': - visitDefaultExportedArrowFunction, + visitExportedFunctionDeclaration, + + 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': + visitExportedVariableDeclaration, 'ExportDefaultDeclaration[declaration.type="Identifier"]': visitDefaultExportedIdentifier, @@ -250,33 +185,137 @@ function getTypeName(typeName: TSESTree.EntityName): string { } } -function findClosestTypeReference( - startNode: TSESTree.Node, - endNode: TSESTree.Node, -): TSESTree.TSTypeReference | null { - let parent = startNode.parent; +function getTypeReferencesRecursively( + node: TSESTree.Node, + sourceCode: TSESLint.SourceCode, +): Set { + const typeReferences = new Set(); - while (parent && parent !== endNode) { - if (parent.type === AST_NODE_TYPES.TSTypeReference) { - return parent; - } + collect(node); - parent = parent.parent; - } + function collect(node: TSESTree.Node): void { + switch (node.type) { + case AST_NODE_TYPES.VariableDeclarator: + collect(node.id); - return null; -} + if (node.init) { + collect(node.init); + } + break; -function isAncestorNode(ancestor: TSESTree.Node, node: TSESTree.Node): boolean { - let parent = node.parent; + case AST_NODE_TYPES.Identifier: { + const typeAnnotation = node.typeAnnotation?.typeAnnotation; - while (parent) { - if (parent === ancestor) { - return true; - } + if (typeAnnotation) { + collect(typeAnnotation); + } + + // If it's a reference to a variable inside an object, we need to get the declared variable. + if ( + node.parent.type === AST_NODE_TYPES.Property || + node.parent.type === AST_NODE_TYPES.ArrayExpression + ) { + const variableNode = sourceCode.getScope(node).set.get(node.name); + + variableNode?.defs.forEach(def => { + collect(def.name); + collect(def.node); + }); + } + + break; + } + + case AST_NODE_TYPES.ObjectExpression: + node.properties.forEach(property => { + const nodeToCheck = + property.type === AST_NODE_TYPES.Property + ? property.value + : property.argument; + + collect(nodeToCheck); + }); + break; + + case AST_NODE_TYPES.ArrayExpression: + node.elements.forEach(element => { + if (!element) { + return; + } + + const nodeToCheck = + element.type === AST_NODE_TYPES.SpreadElement + ? element.argument + : element; + + collect(nodeToCheck); + }); + break; + + case AST_NODE_TYPES.ArrowFunctionExpression: + case AST_NODE_TYPES.FunctionDeclaration: + case AST_NODE_TYPES.FunctionExpression: + case AST_NODE_TYPES.TSDeclareFunction: { + const scope = sourceCode.getScope(node); + + scope.through.forEach(ref => { + collect(ref.identifier.parent); + }); + break; + } - parent = parent.parent; + case AST_NODE_TYPES.TSTypeReference: + typeReferences.add(node); + + node.typeArguments?.params.forEach(param => collect(param)); + break; + + case AST_NODE_TYPES.TSArrayType: + collect(node.elementType); + break; + + case AST_NODE_TYPES.TSTupleType: + node.elementTypes.forEach(element => collect(element)); + break; + + case AST_NODE_TYPES.TSUnionType: + case AST_NODE_TYPES.TSIntersectionType: + node.types.forEach(type => collect(type)); + break; + + case AST_NODE_TYPES.TSTypeLiteral: + node.members.forEach(member => collect(member)); + break; + + case AST_NODE_TYPES.TSPropertySignature: + if (node.typeAnnotation?.typeAnnotation) { + collect(node.typeAnnotation.typeAnnotation); + } + break; + + case AST_NODE_TYPES.TSQualifiedName: + collect(node.parent); + break; + + case AST_NODE_TYPES.TSAsExpression: { + collect(node.expression); + + const isAsConstAnnotation = + node.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && + node.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && + node.typeAnnotation.typeName.name === 'const'; + + if (!isAsConstAnnotation) { + collect(node.typeAnnotation); + } + + break; + } + + default: + break; + } } - return false; + return typeReferences; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index e58a6c55bc0b..7c81df01dd38 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2003,5 +2003,608 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type A = string; + type B = string; + + const apple: A = 'apple'; + const banana: B = 'banana'; + + export const value = { + path: { + to: { + apple, + and: { + banana, + }, + }, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 22, + endColumn: 23, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 23, + endColumn: 24, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = string; + type B = string; + + const apple: A = 'apple'; + const banana: B = 'banana'; + + const value = { + path: { + to: { + apple, + and: { + banana, + }, + }, + }, + }; + + export default value; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 22, + endColumn: 23, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 23, + endColumn: 24, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = string; + type B = string; + + const apple: A = 'apple'; + const banana: B = 'banana'; + + const value = { + spreadObject: { ...{ apple } }, + spreadArray: [...[banana]], + }; + + export default value; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 22, + endColumn: 23, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 23, + endColumn: 24, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type Fruit = 'apple' | 'banana'; + + const apple: Fruit = 'apple'; + const banana: Fruit = 'banana'; + + export const value = { + path: { + to: [apple, banana], + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 22, + endColumn: 27, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + type Fruit = 'apple' | 'banana'; + + const apple: Fruit = 'apple'; + const banana: Fruit = 'banana'; + + export const value = { + path: { + to: [apple, banana] as const, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 22, + endColumn: 27, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + type Fruit = 'apple' | 'banana'; + + const apple: Fruit = 'apple'; + const banana: Fruit = 'banana'; + + export const value = { + path: { + to: [apple, banana] as any, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 22, + endColumn: 27, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + type Fruit = 'apple' | 'banana'; + + const apple = 'apple'; + const banana = 'banana'; + + export const value = { + path: { + to: [apple, banana] as [Fruit, Fruit], + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 9, + column: 37, + endColumn: 42, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + type Fruit = 'apple' | 'banana'; + + const apple = 'apple'; + const banana = 'banana'; + + export const value = { + path: { + to: [apple, banana] as Fruit | number, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 9, + column: 36, + endColumn: 41, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + type C = boolean; + type D = symbol; + + declare const a: [A, B] | ([Array, Set] & number); + + export const value = { a }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 7, + column: 27, + endColumn: 28, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 7, + column: 30, + endColumn: 31, + data: { + name: 'B', + }, + }, + { + messageId: 'requireTypeExport', + line: 7, + column: 43, + endColumn: 44, + data: { + name: 'C', + }, + }, + { + messageId: 'requireTypeExport', + line: 7, + column: 51, + endColumn: 52, + data: { + name: 'D', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + export const value = { + func: (arg: A): B => 'apple', + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 23, + endColumn: 24, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 27, + endColumn: 28, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + export const value = { + func: function (arg: A): B { + return 'apple'; + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 32, + endColumn: 33, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 36, + endColumn: 37, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + const func = (arg: A): B => 'apple'; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 28, + endColumn: 29, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 32, + endColumn: 33, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + const func = function (arg: A): B { + return 'apple'; + }; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 38, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const func = (arg: T): T => 'apple'; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 33, + endColumn: 34, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const func = function (arg: T): T { + return 'apple'; + }; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 42, + endColumn: 43, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + export const value = { + func: (arg: T): T => 'apple', + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 28, + endColumn: 29, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + export const value = { + func: function (arg: T): T { + return 'apple'; + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 37, + endColumn: 38, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + declare function func(arg: T): T; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + declare function func(arg: T): T; + + export const value = { + func, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 41, + endColumn: 46, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + declare const a: Fruit.Apple; + + export const value = { + a, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 26, + endColumn: 37, + data: { + name: 'Fruit', + }, + }, + ], + }, ], }); From dca52d062cd0c390dedb72be97582204c7da9820 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 28 Jun 2024 18:49:50 +0300 Subject: [PATCH 39/66] lib types --- .../src/rules/require-types-exports.ts | 77 ++++++++++--------- .../tests/rules/require-types-exports.test.ts | 43 ++++++++++- 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 44ccdee972da..8cf70d1a13f5 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,12 +1,11 @@ -import { DefinitionType } from '@typescript-eslint/scope-manager'; +import { + DefinitionType, + ImplicitLibVariable, +} from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { - createRule, - getParserServices, - isSymbolFromDefaultLibrary, -} from '../util'; +import { createRule } from '../util'; type MessageIds = 'requireTypeExport'; @@ -25,8 +24,6 @@ export default createRule<[], MessageIds>({ }, defaultOptions: [], create(context) { - const services = getParserServices(context); - const externalizedTypes = new Set(); const reportedTypes = new Set(); @@ -76,7 +73,7 @@ export default createRule<[], MessageIds>({ }, ): void { const scope = context.sourceCode.getScope(node); - const variable = scope.set.get(node.declaration.name); + const variable = getVariable(node.declaration.name, scope); if (!variable) { return; @@ -103,12 +100,6 @@ export default createRule<[], MessageIds>({ function checkTypeNode(node: TSESTree.TSTypeReference): void { const name = getTypeName(node.typeName); - // Using `this` type is allowed since it's necessarily exported - // if it's used in an exported entity. - if (name === 'this') { - return; - } - const isExternalized = externalizedTypes.has(name); const isReported = reportedTypes.has(name); @@ -116,14 +107,6 @@ export default createRule<[], MessageIds>({ return; } - const tsNode = services.esTreeNodeToTSNodeMap.get(node); - const type = services.program.getTypeChecker().getTypeAtLocation(tsNode); - const symbol = type.aliasSymbol ?? type.getSymbol(); - - if (isSymbolFromDefaultLibrary(services.program, symbol)) { - return; - } - context.report({ node: node, messageId: 'requireTypeExport', @@ -210,12 +193,14 @@ function getTypeReferencesRecursively( collect(typeAnnotation); } - // If it's a reference to a variable inside an object, we need to get the declared variable. + // If it's a reference to a variable inside an object or an array, + // we need to get the declared variable. if ( node.parent.type === AST_NODE_TYPES.Property || node.parent.type === AST_NODE_TYPES.ArrayExpression ) { - const variableNode = sourceCode.getScope(node).set.get(node.name); + const scope = sourceCode.getScope(node); + const variableNode = getVariable(node.name, scope); variableNode?.defs.forEach(def => { collect(def.name); @@ -264,11 +249,19 @@ function getTypeReferencesRecursively( break; } - case AST_NODE_TYPES.TSTypeReference: - typeReferences.add(node); + case AST_NODE_TYPES.TSTypeReference: { + const scope = sourceCode.getScope(node); + const variable = getVariable(getTypeName(node.typeName), scope); + + const isBuiltinType = variable instanceof ImplicitLibVariable; + + if (!isBuiltinType) { + typeReferences.add(node); + } node.typeArguments?.params.forEach(param => collect(param)); break; + } case AST_NODE_TYPES.TSArrayType: collect(node.elementType); @@ -299,15 +292,7 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.TSAsExpression: { collect(node.expression); - - const isAsConstAnnotation = - node.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && - node.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && - node.typeAnnotation.typeName.name === 'const'; - - if (!isAsConstAnnotation) { - collect(node.typeAnnotation); - } + collect(node.typeAnnotation); break; } @@ -319,3 +304,23 @@ function getTypeReferencesRecursively( return typeReferences; } + +function getVariable( + name: string, + initialScope: TSESLint.Scope.Scope | null, +): TSESLint.Scope.Variable | null { + let variable: TSESLint.Scope.Variable | null = null; + let scope: TSESLint.Scope.Scope | null = initialScope; + + while (scope) { + variable = scope.set.get(name) ?? null; + + if (variable) { + break; + } + + scope = scope.upper; + } + + return variable; +} diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 7c81df01dd38..c80d0ee13039 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -403,6 +403,47 @@ ruleTester.run('require-types-exports', rule, { return arg; } `, + + ` + declare const element: HTMLElement; + + export default element; + `, + + ` + export const date: Date = new Date(); + `, + + ` + import ts from 'typescript'; + + export enum Fruit { + Apple, + Banana, + Cherry, + } + + declare const apple: Fruit.Apple; + + export type A = number; + export type B = string; + export type C = boolean; + + export interface D { + key: string; + } + + function func>( + arg: T, + ): T | ts.Type { + return arg; + } + + export const value = { + apple, + func, + }; + `, ], invalid: [ @@ -2262,7 +2303,7 @@ ruleTester.run('require-types-exports', rule, { type C = boolean; type D = symbol; - declare const a: [A, B] | ([Array, Set] & number); + declare const a: [A, B] | ([Array, Set] & Exclude); export const value = { a }; `, From 15fc51cf52d88b2f9954ad34587547fcc8c4b547 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Fri, 28 Jun 2024 18:54:26 +0300 Subject: [PATCH 40/66] style --- packages/eslint-plugin/src/rules/require-types-exports.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 8cf70d1a13f5..c1e618373a86 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -235,6 +235,7 @@ function getTypeReferencesRecursively( collect(nodeToCheck); }); + break; case AST_NODE_TYPES.ArrowFunctionExpression: @@ -246,6 +247,7 @@ function getTypeReferencesRecursively( scope.through.forEach(ref => { collect(ref.identifier.parent); }); + break; } @@ -284,18 +286,17 @@ function getTypeReferencesRecursively( if (node.typeAnnotation?.typeAnnotation) { collect(node.typeAnnotation.typeAnnotation); } + break; case AST_NODE_TYPES.TSQualifiedName: collect(node.parent); break; - case AST_NODE_TYPES.TSAsExpression: { + case AST_NODE_TYPES.TSAsExpression: collect(node.expression); collect(node.typeAnnotation); - break; - } default: break; From 59eda588f37e2615b05e274e3756e0b6a38166f1 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sat, 29 Jun 2024 22:07:19 +0300 Subject: [PATCH 41/66] wip --- .../src/rules/require-types-exports.ts | 6 +++ .../tests/rules/require-types-exports.test.ts | 51 ++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index c1e618373a86..259d8294436e 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -238,6 +238,12 @@ function getTypeReferencesRecursively( break; + case AST_NODE_TYPES.NewExpression: + collect(node.callee); + node.arguments.forEach(arg => collect(arg)); + node.typeArguments?.params.forEach(param => collect(param)); + break; + case AST_NODE_TYPES.ArrowFunctionExpression: case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index c80d0ee13039..44d0a9fe4dd4 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -416,29 +416,29 @@ ruleTester.run('require-types-exports', rule, { ` import ts from 'typescript'; - + export enum Fruit { Apple, Banana, Cherry, } - + declare const apple: Fruit.Apple; - + export type A = number; export type B = string; export type C = boolean; - + export interface D { key: string; } - + function func>( arg: T, ): T | ts.Type { return arg; } - + export const value = { apple, func, @@ -2647,5 +2647,44 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type Item = { + key: string; + value: number; + }; + + type ItemKey = Item['key']; + + const item: Item = { key: 'apple', value: 1 }; + + const map = new Map([['apple', item]]); + + export const value = { + map, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 9, + column: 21, + endColumn: 25, + data: { + name: 'Item', + }, + }, + { + messageId: 'requireTypeExport', + line: 11, + column: 29, + endColumn: 36, + data: { + name: 'ItemKey', + }, + }, + ], + }, ], }); From fc0858a37e44812df7ce7c5446e2e3e5cdf82c61 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sat, 29 Jun 2024 22:41:38 +0300 Subject: [PATCH 42/66] wip --- .../src/rules/require-types-exports.ts | 44 +++--- .../tests/rules/require-types-exports.test.ts | 125 ++++++++++++++++++ 2 files changed, 154 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 259d8294436e..19937a0a3901 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -173,10 +173,17 @@ function getTypeReferencesRecursively( sourceCode: TSESLint.SourceCode, ): Set { const typeReferences = new Set(); + const visited = new Set(); collect(node); function collect(node: TSESTree.Node): void { + if (visited.has(node)) { + return; + } + + visited.add(node); + switch (node.type) { case AST_NODE_TYPES.VariableDeclarator: collect(node.id); @@ -193,20 +200,14 @@ function getTypeReferencesRecursively( collect(typeAnnotation); } - // If it's a reference to a variable inside an object or an array, - // we need to get the declared variable. - if ( - node.parent.type === AST_NODE_TYPES.Property || - node.parent.type === AST_NODE_TYPES.ArrayExpression - ) { - const scope = sourceCode.getScope(node); - const variableNode = getVariable(node.name, scope); - - variableNode?.defs.forEach(def => { - collect(def.name); - collect(def.node); - }); - } + // Resolve the variable to its declaration (in cases where the variable is referenced) + const scope = sourceCode.getScope(node); + const variableNode = getVariable(node.name, scope); + + variableNode?.defs.forEach(def => { + collect(def.name); + collect(def.node); + }); break; } @@ -239,6 +240,7 @@ function getTypeReferencesRecursively( break; case AST_NODE_TYPES.NewExpression: + case AST_NODE_TYPES.CallExpression: collect(node.callee); node.arguments.forEach(arg => collect(arg)); node.typeArguments?.params.forEach(param => collect(param)); @@ -251,12 +253,24 @@ function getTypeReferencesRecursively( const scope = sourceCode.getScope(node); scope.through.forEach(ref => { - collect(ref.identifier.parent); + const isSelfReference = ref.identifier.parent === node; + + const nodeToCheck = isSelfReference + ? ref.identifier + : ref.identifier.parent; + + collect(nodeToCheck); }); break; } + case AST_NODE_TYPES.ReturnStatement: + if (node.argument) { + collect(node.argument); + } + break; + case AST_NODE_TYPES.TSTypeReference: { const scope = sourceCode.getScope(node); const variable = getVariable(getTypeName(node.typeName), scope); diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 44d0a9fe4dd4..b613d38f8ecb 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2648,6 +2648,62 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + declare const a: Fruit.Apple; + + export const value = { + key: () => a, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 26, + endColumn: 37, + data: { + name: 'Fruit', + }, + }, + ], + }, + + { + code: ` + enum Fruit { + Apple, + Banana, + Cherry, + } + + declare const a: Fruit.Apple; + + export const value = { + key: function () { + return a; + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 26, + endColumn: 37, + data: { + name: 'Fruit', + }, + }, + ], + }, + { code: ` type Item = { @@ -2686,5 +2742,74 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type A = number; + + const item: A = 1; + + export const value = { + key: (() => item)(), + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 21, + endColumn: 22, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const item: A = 1; + + export const value = { + key: ((a: A) => a)(item), + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 7, + column: 21, + endColumn: 22, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const item: A = 1; + + export const value = { + key: ((a: T) => a)(item), + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 7, + column: 28, + endColumn: 29, + data: { + name: 'A', + }, + }, + ], + }, ], }); From 94a98ebc95f752dbac8b64b25650cc2670e40388 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sat, 29 Jun 2024 23:07:16 +0300 Subject: [PATCH 43/66] configs --- packages/eslint-plugin/src/configs/disable-type-checked.ts | 1 - packages/eslint-plugin/src/configs/strict-type-checked-only.ts | 1 - packages/eslint-plugin/src/configs/strict.ts | 1 + packages/typescript-eslint/src/configs/disable-type-checked.ts | 1 - .../typescript-eslint/src/configs/strict-type-checked-only.ts | 1 - packages/typescript-eslint/src/configs/strict.ts | 1 + 6 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 3e5502db8479..8e47f9688d73 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -58,7 +58,6 @@ export = { '@typescript-eslint/promise-function-async': 'off', '@typescript-eslint/require-array-sort-compare': 'off', '@typescript-eslint/require-await': 'off', - '@typescript-eslint/require-types-exports': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 5330f50018a3..a17bc16c257e 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -43,7 +43,6 @@ export = { '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': [ 'error', { diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index 9c51d5c47348..cb825c95d3cb 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -40,6 +40,7 @@ export = { '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-literal-enum-member': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unified-signatures': 'error', }, diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 3dd4b859a2bf..3018fcabd9a8 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -61,7 +61,6 @@ export default ( '@typescript-eslint/promise-function-async': 'off', '@typescript-eslint/require-array-sort-compare': 'off', '@typescript-eslint/require-await': 'off', - '@typescript-eslint/require-types-exports': 'off', '@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/return-await': 'off', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index 47c853df2b98..cd45ade64d2f 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -52,7 +52,6 @@ export default ( '@typescript-eslint/prefer-return-this-type': 'error', 'require-await': 'off', '@typescript-eslint/require-await': 'error', - '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/restrict-plus-operands': [ 'error', { diff --git a/packages/typescript-eslint/src/configs/strict.ts b/packages/typescript-eslint/src/configs/strict.ts index 71615896c4dd..f208b0a4a286 100644 --- a/packages/typescript-eslint/src/configs/strict.ts +++ b/packages/typescript-eslint/src/configs/strict.ts @@ -49,6 +49,7 @@ export default ( '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-literal-enum-member': 'error', + '@typescript-eslint/require-types-exports': 'error', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unified-signatures': 'error', }, From dee0fe460906564c82415d7a7a1a2708311b6ad2 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sat, 29 Jun 2024 23:21:29 +0300 Subject: [PATCH 44/66] don't report generic params in call expression --- .../src/rules/require-types-exports.ts | 9 +++++++- .../tests/rules/require-types-exports.test.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 19937a0a3901..bf376cbd00b4 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,6 +1,7 @@ import { DefinitionType, ImplicitLibVariable, + ScopeType, } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -277,7 +278,13 @@ function getTypeReferencesRecursively( const isBuiltinType = variable instanceof ImplicitLibVariable; - if (!isBuiltinType) { + const isGenericTypeArg = + variable?.scope.type === ScopeType.function && + variable.identifiers.every( + id => id.parent.type === AST_NODE_TYPES.TSTypeParameter, + ); + + if (!isBuiltinType && !isGenericTypeArg) { typeReferences.add(node); } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index b613d38f8ecb..dced7772d9b6 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2811,5 +2811,28 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type A = number; + + export function func1(arg: R): R { + return func2(arg); + } + + declare function func2(arg: T): T; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, ], }); From 0804b241d77c47e8fe66ed09e90ea0d14983e8dd Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 08:27:25 +0300 Subject: [PATCH 45/66] improve function types collection --- .../src/rules/require-types-exports.ts | 83 +++++++++++------- .../tests/rules/require-types-exports.test.ts | 85 +++++++++++++++++++ 2 files changed, 137 insertions(+), 31 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index bf376cbd00b4..d2da69bd844f 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -178,7 +178,11 @@ function getTypeReferencesRecursively( collect(node); - function collect(node: TSESTree.Node): void { + function collect(node: TSESTree.Node | null | undefined): void { + if (!node) { + return; + } + if (visited.has(node)) { return; } @@ -195,11 +199,7 @@ function getTypeReferencesRecursively( break; case AST_NODE_TYPES.Identifier: { - const typeAnnotation = node.typeAnnotation?.typeAnnotation; - - if (typeAnnotation) { - collect(typeAnnotation); - } + collect(node.typeAnnotation?.typeAnnotation); // Resolve the variable to its declaration (in cases where the variable is referenced) const scope = sourceCode.getScope(node); @@ -226,12 +226,8 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.ArrayExpression: node.elements.forEach(element => { - if (!element) { - return; - } - const nodeToCheck = - element.type === AST_NODE_TYPES.SpreadElement + element?.type === AST_NODE_TYPES.SpreadElement ? element.argument : element; @@ -250,26 +246,53 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.ArrowFunctionExpression: case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: - case AST_NODE_TYPES.TSDeclareFunction: { - const scope = sourceCode.getScope(node); - - scope.through.forEach(ref => { - const isSelfReference = ref.identifier.parent === node; + case AST_NODE_TYPES.TSDeclareFunction: + node.typeParameters?.params.forEach(param => collect(param.constraint)); + node.params.forEach(collect); + + /** + * If there is a return type annotation - collect the types from there. + * Otherwise - infer the return type from the return statements. + */ + if (node.returnType) { + collect(node.returnType.typeAnnotation); + } else { + collect(node.body); + } - const nodeToCheck = isSelfReference - ? ref.identifier - : ref.identifier.parent; + break; - collect(nodeToCheck); + case AST_NODE_TYPES.BlockStatement: + node.body.forEach(item => { + if (item.type === AST_NODE_TYPES.ReturnStatement) { + collect(item); + } }); + break; + + case AST_NODE_TYPES.AssignmentPattern: + collect(node.left); + break; + + case AST_NODE_TYPES.RestElement: + collect(node.argument); + collect(node.typeAnnotation?.typeAnnotation); + break; + + case AST_NODE_TYPES.ObjectPattern: + node.properties.forEach(collect); + collect(node.typeAnnotation?.typeAnnotation); + + break; + + case AST_NODE_TYPES.ArrayPattern: + node.elements.forEach(collect); + collect(node.typeAnnotation?.typeAnnotation); break; - } case AST_NODE_TYPES.ReturnStatement: - if (node.argument) { - collect(node.argument); - } + collect(node.argument); break; case AST_NODE_TYPES.TSTypeReference: { @@ -288,7 +311,7 @@ function getTypeReferencesRecursively( typeReferences.add(node); } - node.typeArguments?.params.forEach(param => collect(param)); + node.typeArguments?.params.forEach(collect); break; } @@ -297,22 +320,20 @@ function getTypeReferencesRecursively( break; case AST_NODE_TYPES.TSTupleType: - node.elementTypes.forEach(element => collect(element)); + node.elementTypes.forEach(collect); break; case AST_NODE_TYPES.TSUnionType: case AST_NODE_TYPES.TSIntersectionType: - node.types.forEach(type => collect(type)); + node.types.forEach(collect); break; case AST_NODE_TYPES.TSTypeLiteral: - node.members.forEach(member => collect(member)); + node.members.forEach(collect); break; case AST_NODE_TYPES.TSPropertySignature: - if (node.typeAnnotation?.typeAnnotation) { - collect(node.typeAnnotation.typeAnnotation); - } + collect(node.typeAnnotation?.typeAnnotation); break; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index dced7772d9b6..47ec6ba0c309 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2834,5 +2834,90 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type A = number; + type B = string; + + export function func1(arg: R): R { + doWork(String(arg)); + + return arg; + } + + declare function doWork(arg: B): void; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = number; + + export function func1(arg: R) { + return func2(arg); + } + + declare function func2(arg: B): B; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 9, + column: 37, + endColumn: 38, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + export function func1(arg: R): R { + function doWork(arg2: B): void {} + + doWork(String(arg)); + + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, ], }); From a0a4944e91a5b1c6022811e4c27099c28f2c702c Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 09:43:33 +0300 Subject: [PATCH 46/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index d2da69bd844f..2cd0c8434bde 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -192,10 +192,7 @@ function getTypeReferencesRecursively( switch (node.type) { case AST_NODE_TYPES.VariableDeclarator: collect(node.id); - - if (node.init) { - collect(node.init); - } + collect(node.init); break; case AST_NODE_TYPES.Identifier: { @@ -334,7 +331,6 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.TSPropertySignature: collect(node.typeAnnotation?.typeAnnotation); - break; case AST_NODE_TYPES.TSQualifiedName: From 66a0affc6cbdfe21ee8a9fdb5ee9499600c0b986 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 10:07:00 +0300 Subject: [PATCH 47/66] wip --- .../src/rules/require-types-exports.ts | 5 +++ .../tests/rules/require-types-exports.test.ts | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 2cd0c8434bde..2d54c39fb32d 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -342,6 +342,11 @@ function getTypeReferencesRecursively( collect(node.typeAnnotation); break; + case AST_NODE_TYPES.TSIndexedAccessType: + collect(node.objectType); + collect(node.indexType); + break; + default: break; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 47ec6ba0c309..d8385868a6d1 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2919,5 +2919,36 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type ItemsMap = Record; + type Key = keyof ItemsMap; + + export function get(key: K): ItemsMap[K] { + return key as never; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 39, + endColumn: 42, + data: { + name: 'Key', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 53, + endColumn: 61, + data: { + name: 'ItemsMap', + }, + }, + ], + }, ], }); From b67e1f990f60ab23be175b7459d533f2938cd9cc Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 14:22:35 +0300 Subject: [PATCH 48/66] remove `getVariable` --- packages/eslint-plugin/src/rules/require-types-exports.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 2d54c39fb32d..69bc558c6857 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -6,7 +6,7 @@ import { import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule } from '../util'; +import { createRule, findVariable } from '../util'; type MessageIds = 'requireTypeExport'; @@ -74,7 +74,7 @@ export default createRule<[], MessageIds>({ }, ): void { const scope = context.sourceCode.getScope(node); - const variable = getVariable(node.declaration.name, scope); + const variable = findVariable(scope, node.declaration.name); if (!variable) { return; @@ -200,7 +200,7 @@ function getTypeReferencesRecursively( // Resolve the variable to its declaration (in cases where the variable is referenced) const scope = sourceCode.getScope(node); - const variableNode = getVariable(node.name, scope); + const variableNode = findVariable(scope, node.name); variableNode?.defs.forEach(def => { collect(def.name); @@ -294,7 +294,7 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.TSTypeReference: { const scope = sourceCode.getScope(node); - const variable = getVariable(getTypeName(node.typeName), scope); + const variable = findVariable(scope, getTypeName(node.typeName)); const isBuiltinType = variable instanceof ImplicitLibVariable; From 9891e78d2c2c71fe6011ef6741c04780843d01db Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 16:33:43 +0300 Subject: [PATCH 49/66] infer return type from return statements --- .../src/rules/require-types-exports.ts | 53 +++++++++---- .../tests/rules/require-types-exports.test.ts | 76 +++++++++++++++++++ packages/utils/src/ts-estree.ts | 2 + 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 69bc558c6857..2339df950c65 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -4,7 +4,7 @@ import { ScopeType, } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, simpleTraverse } from '@typescript-eslint/utils'; import { createRule, findVariable } from '../util'; @@ -254,7 +254,7 @@ function getTypeReferencesRecursively( if (node.returnType) { collect(node.returnType.typeAnnotation); } else { - collect(node.body); + collectFunctionReturnStatements(node).forEach(collect); } break; @@ -355,22 +355,45 @@ function getTypeReferencesRecursively( return typeReferences; } -function getVariable( - name: string, - initialScope: TSESLint.Scope.Scope | null, -): TSESLint.Scope.Variable | null { - let variable: TSESLint.Scope.Variable | null = null; - let scope: TSESLint.Scope.Scope | null = initialScope; +function collectFunctionReturnStatements( + functionNode: TSESTree.Node, +): Set { + const isArrowFunctionReturn = + functionNode.type === AST_NODE_TYPES.ArrowFunctionExpression && + functionNode.body.type === AST_NODE_TYPES.Identifier; - while (scope) { - variable = scope.set.get(name) ?? null; + if (isArrowFunctionReturn) { + return new Set([functionNode.body]); + } - if (variable) { - break; - } + const returnStatements = new Set(); + + simpleTraverse(functionNode, { + visitors: { + ReturnStatement: (node: TSESTree.Node) => { + if (getParentFunction(node) === functionNode) { + returnStatements.add(node); + } + }, + }, + }); + + return returnStatements; +} + +function getParentFunction(node: TSESTree.Node): TSESTree.Node | null { + let parent: TSESTree.Node | undefined = node.parent; + + const functionTypes = new Set([ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.TSDeclareFunction, + ]); - scope = scope.upper; + while (parent && !functionTypes.has(parent.type)) { + parent = parent.parent; } - return variable; + return parent ?? null; } diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index d8385868a6d1..26f0727da618 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2894,6 +2894,82 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type A = number; + type B = number; + type C = number; + + export function func1(arg: R) { + if (Math.random() > 0.5) { + return func2(arg); + } else { + return func3(arg); + } + } + + declare function func2(arg: B): B; + declare function func3(arg: C): C; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 14, + column: 37, + endColumn: 38, + data: { + name: 'B', + }, + }, + { + messageId: 'requireTypeExport', + line: 15, + column: 37, + endColumn: 38, + data: { + name: 'C', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = number; + + export function func1(arg: R) { + const a = (() => { + return func2(arg); + })(); + + return arg; + } + + declare function func2(arg: B): B; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, + { code: ` type A = number; diff --git a/packages/utils/src/ts-estree.ts b/packages/utils/src/ts-estree.ts index 212d339c4ba3..fb6f5b34eb6e 100644 --- a/packages/utils/src/ts-estree.ts +++ b/packages/utils/src/ts-estree.ts @@ -12,3 +12,5 @@ export type { ParserServicesWithTypeInformation, ParserServicesWithoutTypeInformation, } from '@typescript-eslint/typescript-estree'; + +export { simpleTraverse } from '@typescript-eslint/typescript-estree'; From cb90d4305b276d72d4e86f29d79155efaa97161f Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 16:42:23 +0300 Subject: [PATCH 50/66] wip --- .../src/rules/require-types-exports.ts | 7 ++--- .../tests/rules/require-types-exports.test.ts | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 2339df950c65..95987dde4250 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -251,11 +251,8 @@ function getTypeReferencesRecursively( * If there is a return type annotation - collect the types from there. * Otherwise - infer the return type from the return statements. */ - if (node.returnType) { - collect(node.returnType.typeAnnotation); - } else { - collectFunctionReturnStatements(node).forEach(collect); - } + collect(node.returnType?.typeAnnotation); + collectFunctionReturnStatements(node).forEach(collect); break; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 26f0727da618..2acbbda6957b 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2970,6 +2970,37 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type A = number; + type B = number; + + export function func1(arg: R) { + return arg as B; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 41, + endColumn: 42, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 25, + endColumn: 26, + data: { + name: 'B', + }, + }, + ], + }, + { code: ` type A = number; From 479f593c23d30040b5aeb2c9ba4cbd4f95e4ff2b Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 17:44:40 +0300 Subject: [PATCH 51/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 95987dde4250..4e7d800c1093 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -246,11 +246,6 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.TSDeclareFunction: node.typeParameters?.params.forEach(param => collect(param.constraint)); node.params.forEach(collect); - - /** - * If there is a return type annotation - collect the types from there. - * Otherwise - infer the return type from the return statements. - */ collect(node.returnType?.typeAnnotation); collectFunctionReturnStatements(node).forEach(collect); From e86427fab8f3487eeec9a671ca606c47c97c0cf1 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 18:00:33 +0300 Subject: [PATCH 52/66] wip --- .../src/rules/require-types-exports.ts | 11 ++ .../tests/rules/require-types-exports.test.ts | 101 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 4e7d800c1093..bc482d3a84ab 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -240,6 +240,17 @@ function getTypeReferencesRecursively( node.typeArguments?.params.forEach(param => collect(param)); break; + case AST_NODE_TYPES.BinaryExpression: + case AST_NODE_TYPES.LogicalExpression: + collect(node.left); + collect(node.right); + break; + + case AST_NODE_TYPES.ConditionalExpression: + collect(node.consequent); + collect(node.alternate); + break; + case AST_NODE_TYPES.ArrowFunctionExpression: case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 2acbbda6957b..c6cd866dbff3 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -3057,5 +3057,106 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + skip: true, + only: true, + code: ` + type A = number; + type B = string; + type C = boolean; + + const obj: { key: { to: A; and: C } } = { + key: { + to: 1, + and: true, + }, + }; + + const obj2 = { + b: 'asd' as B, + }; + + export function func() { + if (Math.random() > 0.5) { + return obj.key; + } + + if (Math.random() < 0.5) { + return obj.key.to; + } + + return obj.key.to + obj2.b + 'asd'; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 39, + endColumn: 42, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const value: A = 1; + + export function func() { + return Math.random() > 0.5 && value; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 22, + endColumn: 23, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + type B = string; + + const valueA: A = 1; + const valueB: B = 'test'; + + export function func() { + return Math.random() > 0.5 ? valueA : valueB; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 23, + endColumn: 24, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 6, + column: 23, + endColumn: 24, + data: { + name: 'B', + }, + }, + ], + }, ], }); From a61d49fdca423c63171a4b9e4e132544cf2dda07 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 18:36:57 +0300 Subject: [PATCH 53/66] wip --- .../tests/rules/require-types-exports.test.ts | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index c6cd866dbff3..d63a45a9f432 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -3058,50 +3058,6 @@ ruleTester.run('require-types-exports', rule, { ], }, - { - skip: true, - only: true, - code: ` - type A = number; - type B = string; - type C = boolean; - - const obj: { key: { to: A; and: C } } = { - key: { - to: 1, - and: true, - }, - }; - - const obj2 = { - b: 'asd' as B, - }; - - export function func() { - if (Math.random() > 0.5) { - return obj.key; - } - - if (Math.random() < 0.5) { - return obj.key.to; - } - - return obj.key.to + obj2.b + 'asd'; - } - `, - errors: [ - { - messageId: 'requireTypeExport', - line: 5, - column: 39, - endColumn: 42, - data: { - name: 'A', - }, - }, - ], - }, - { code: ` type A = number; From 121f47572a0183867a4fff096c23d977048fc6c1 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 18:37:31 +0300 Subject: [PATCH 54/66] wip --- packages/eslint-plugin/src/rules/require-types-exports.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index bc482d3a84ab..da478b78e8f7 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -179,11 +179,7 @@ function getTypeReferencesRecursively( collect(node); function collect(node: TSESTree.Node | null | undefined): void { - if (!node) { - return; - } - - if (visited.has(node)) { + if (!node || visited.has(node)) { return; } From f3f8518b80fc2ce3f2ee11be9037f1be7c666c4b Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 30 Jun 2024 18:45:33 +0300 Subject: [PATCH 55/66] wip --- .../eslint-plugin/src/rules/require-types-exports.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index da478b78e8f7..c3c0f3c7be1f 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -202,7 +202,6 @@ function getTypeReferencesRecursively( collect(def.name); collect(def.node); }); - break; } @@ -226,7 +225,6 @@ function getTypeReferencesRecursively( collect(nodeToCheck); }); - break; case AST_NODE_TYPES.NewExpression: @@ -255,15 +253,6 @@ function getTypeReferencesRecursively( node.params.forEach(collect); collect(node.returnType?.typeAnnotation); collectFunctionReturnStatements(node).forEach(collect); - - break; - - case AST_NODE_TYPES.BlockStatement: - node.body.forEach(item => { - if (item.type === AST_NODE_TYPES.ReturnStatement) { - collect(item); - } - }); break; case AST_NODE_TYPES.AssignmentPattern: @@ -278,7 +267,6 @@ function getTypeReferencesRecursively( case AST_NODE_TYPES.ObjectPattern: node.properties.forEach(collect); collect(node.typeAnnotation?.typeAnnotation); - break; case AST_NODE_TYPES.ArrayPattern: From ab837b455e45207d06c7ec72fe14192660dcea40 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 20:01:15 +0300 Subject: [PATCH 56/66] custom traversal --- .../src/rules/require-types-exports.ts | 84 +++++++++++++------ packages/utils/src/ts-estree.ts | 2 - 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index c3c0f3c7be1f..9de97daca013 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -4,7 +4,7 @@ import { ScopeType, } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES, simpleTraverse } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, findVariable } from '../util'; @@ -252,7 +252,10 @@ function getTypeReferencesRecursively( node.typeParameters?.params.forEach(param => collect(param.constraint)); node.params.forEach(collect); collect(node.returnType?.typeAnnotation); - collectFunctionReturnStatements(node).forEach(collect); + + if (node.body) { + collectFunctionReturnStatements(node).forEach(collect); + } break; case AST_NODE_TYPES.AssignmentPattern: @@ -343,11 +346,14 @@ function getTypeReferencesRecursively( } function collectFunctionReturnStatements( - functionNode: TSESTree.Node, + functionNode: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, ): Set { const isArrowFunctionReturn = functionNode.type === AST_NODE_TYPES.ArrowFunctionExpression && - functionNode.body.type === AST_NODE_TYPES.Identifier; + functionNode.body.type !== AST_NODE_TYPES.BlockStatement; if (isArrowFunctionReturn) { return new Set([functionNode.body]); @@ -355,32 +361,58 @@ function collectFunctionReturnStatements( const returnStatements = new Set(); - simpleTraverse(functionNode, { - visitors: { - ReturnStatement: (node: TSESTree.Node) => { - if (getParentFunction(node) === functionNode) { - returnStatements.add(node); - } - }, - }, - }); + forEachReturnStatement(functionNode, returnNode => + returnStatements.add(returnNode), + ); return returnStatements; } -function getParentFunction(node: TSESTree.Node): TSESTree.Node | null { - let parent: TSESTree.Node | undefined = node.parent; - - const functionTypes = new Set([ - AST_NODE_TYPES.ArrowFunctionExpression, - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.TSDeclareFunction, - ]); +// Heavily inspired by: +// https://github.com/typescript-eslint/typescript-eslint/blob/103de6eed/packages/eslint-plugin/src/util/astUtils.ts#L47-L80 +export function forEachReturnStatement( + functionNode: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + visitor: (returnNode: TSESTree.ReturnStatement) => void, +): void { + return traverse(functionNode.body); + + function traverse(node: TSESTree.Node | null): void { + switch (node?.type) { + case AST_NODE_TYPES.ReturnStatement: + return visitor(node); + + case AST_NODE_TYPES.SwitchStatement: + return node.cases.forEach(traverse); + + case AST_NODE_TYPES.SwitchCase: + return node.consequent.forEach(traverse); + + case AST_NODE_TYPES.BlockStatement: + return node.body.forEach(traverse); + + case AST_NODE_TYPES.DoWhileStatement: + case AST_NODE_TYPES.ForInStatement: + case AST_NODE_TYPES.ForOfStatement: + case AST_NODE_TYPES.WhileStatement: + case AST_NODE_TYPES.ForStatement: + case AST_NODE_TYPES.WithStatement: + case AST_NODE_TYPES.CatchClause: + case AST_NODE_TYPES.LabeledStatement: + return traverse(node.body); + + case AST_NODE_TYPES.IfStatement: + traverse(node.consequent); + traverse(node.alternate); + return; - while (parent && !functionTypes.has(parent.type)) { - parent = parent.parent; + case AST_NODE_TYPES.TryStatement: + traverse(node.block); + traverse(node.handler); + traverse(node.finalizer); + return; + } } - - return parent ?? null; } diff --git a/packages/utils/src/ts-estree.ts b/packages/utils/src/ts-estree.ts index fb6f5b34eb6e..212d339c4ba3 100644 --- a/packages/utils/src/ts-estree.ts +++ b/packages/utils/src/ts-estree.ts @@ -12,5 +12,3 @@ export type { ParserServicesWithTypeInformation, ParserServicesWithoutTypeInformation, } from '@typescript-eslint/typescript-estree'; - -export { simpleTraverse } from '@typescript-eslint/typescript-estree'; From 1641272bffa5c324091991cdce2cdaaa47fba592 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 20:06:52 +0300 Subject: [PATCH 57/66] some tests --- .../tests/rules/require-types-exports.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index d63a45a9f432..095d93f378e8 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -2812,6 +2812,52 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + type A = number; + + const item: A = 1; + + export const value = { + key: ((a: A) => [a])(item), + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 7, + column: 21, + endColumn: 22, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + const item: A = 1; + + export const value = { + key: ((a: A) => ({ a }))(item), + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 7, + column: 21, + endColumn: 22, + data: { + name: 'A', + }, + }, + ], + }, + { code: ` type A = number; From fd56a1c7cd14f8ccb44b4173730de8509d7dbac3 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 20:37:28 +0300 Subject: [PATCH 58/66] add missing tests --- .../tests/rules/require-types-exports.test.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 095d93f378e8..5c410b35162a 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -1906,6 +1906,113 @@ ruleTester.run('require-types-exports', rule, { ], }, + { + code: ` + namespace A { + export type B = number; + } + + type B = string; + + export function a(arg: B) { + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 8, + column: 32, + endColumn: 33, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + namespace A { + export interface B { + value: number; + } + } + + type B = string; + + export function a(arg: B) { + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 10, + column: 32, + endColumn: 33, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + namespace A { + export enum B { + Value1, + Value2, + } + } + + type B = string; + + export function a(arg: B) { + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 11, + column: 32, + endColumn: 33, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + namespace A { + export namespace B { + export type C = number; + } + } + + type B = string; + + export function a(arg: B) { + return arg; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 10, + column: 32, + endColumn: 33, + data: { + name: 'B', + }, + }, + ], + }, + { code: ` import type { A } from './types'; From b0613d5cdad57ae64ac902f6181d2eadb0d9def7 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 20:43:28 +0300 Subject: [PATCH 59/66] report default exported call expression --- .../src/rules/require-types-exports.ts | 25 +++---------------- .../tests/fixtures/tsconfig-with-dom.json | 9 +++++++ .../tests/rules/require-types-exports.test.ts | 23 ++++++++++++++++- 3 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 9de97daca013..80b53eaa6612 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,5 +1,4 @@ import { - DefinitionType, ImplicitLibVariable, ScopeType, } from '@typescript-eslint/scope-manager'; @@ -68,25 +67,10 @@ export default createRule<[], MessageIds>({ } } - function visitDefaultExportedIdentifier( - node: TSESTree.DefaultExportDeclarations & { - declaration: TSESTree.Identifier; - }, + function visitExportDefaultDeclaration( + node: TSESTree.ExportDefaultDeclaration, ): void { - const scope = context.sourceCode.getScope(node); - const variable = findVariable(scope, node.declaration.name); - - if (!variable) { - return; - } - - for (const def of variable.defs) { - if (def.type !== DefinitionType.Variable || !def.node.init) { - continue; - } - - checkNodeTypes(def.node); - } + checkNodeTypes(node.declaration); } function checkNodeTypes(node: TSESTree.Node): void { @@ -148,8 +132,7 @@ export default createRule<[], MessageIds>({ 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': visitExportedVariableDeclaration, - 'ExportDefaultDeclaration[declaration.type="Identifier"]': - visitDefaultExportedIdentifier, + ExportDefaultDeclaration: visitExportDefaultDeclaration, }; }, }); diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json b/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json new file mode 100644 index 000000000000..87cfb2c4b422 --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "ESNext", + "strict": true, + "esModuleInterop": true, + "lib": ["esnext", "DOM"] + } +} diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 5c410b35162a..e3bf4f43cf89 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -9,7 +9,7 @@ const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { tsconfigRootDir: rootPath, - project: './tsconfig.json', + project: './tsconfig-with-dom.json', }, }); @@ -3267,5 +3267,26 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + declare function func(): string; + + type A = string; + + export default func(); + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 6, + column: 29, + endColumn: 30, + data: { + name: 'A', + }, + }, + ], + }, ], }); From b9f11483cde5c3f61d3073fabb2c5a2bf1454e03 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 22:40:32 +0300 Subject: [PATCH 60/66] report types used within exported types --- .../src/rules/require-types-exports.ts | 36 ++++++ .../tests/rules/require-types-exports.test.ts | 103 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 80b53eaa6612..71414d6c5fcf 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -67,6 +67,22 @@ export default createRule<[], MessageIds>({ } } + function visitExportedTypeAliasDeclaration( + node: TSESTree.ExportNamedDeclaration & { + declaration: TSESTree.TSTypeAliasDeclaration; + }, + ): void { + checkNodeTypes(node.declaration.typeAnnotation); + } + + function visitExportedInterfaceDeclaration( + node: TSESTree.ExportNamedDeclaration & { + declaration: TSESTree.TSInterfaceDeclaration; + }, + ): void { + checkNodeTypes(node.declaration.body); + } + function visitExportDefaultDeclaration( node: TSESTree.ExportDefaultDeclaration, ): void { @@ -132,6 +148,18 @@ export default createRule<[], MessageIds>({ 'ExportNamedDeclaration[declaration.type="VariableDeclaration"]': visitExportedVariableDeclaration, + 'ExportNamedDeclaration[declaration.type="TSTypeAliasDeclaration"]': + visitExportedTypeAliasDeclaration, + + 'ExportNamedDeclaration[declaration.type="TSTypeAliasDeclaration"] > ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': + visitExportedTypeAliasDeclaration, + + 'ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': + visitExportedInterfaceDeclaration, + + 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"] > ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': + visitExportedInterfaceDeclaration, + ExportDefaultDeclaration: visitExportDefaultDeclaration, }; }, @@ -302,6 +330,14 @@ function getTypeReferencesRecursively( node.members.forEach(collect); break; + case AST_NODE_TYPES.TSTemplateLiteralType: + node.types.forEach(collect); + break; + + case AST_NODE_TYPES.TSInterfaceBody: + node.body.forEach(collect); + break; + case AST_NODE_TYPES.TSPropertySignature: collect(node.typeAnnotation?.typeAnnotation); break; diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index e3bf4f43cf89..b0dde505720b 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -3288,5 +3288,108 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + type Apple = 'apple'; + type Banana = 'banana'; + + export type Fruites = Apple | Banana; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 31, + endColumn: 36, + data: { + name: 'Apple', + }, + }, + { + messageId: 'requireTypeExport', + line: 5, + column: 39, + endColumn: 45, + data: { + name: 'Banana', + }, + }, + ], + }, + + { + code: ` + type A = number; + + export interface B { + a: A; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 14, + endColumn: 15, + data: { + name: 'A', + }, + }, + ], + }, + + { + code: ` + type A = number; + + interface B { + b: string; + } + + export namespace C { + export type D = A; + export type E = B; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 9, + column: 27, + endColumn: 28, + data: { + name: 'A', + }, + }, + { + messageId: 'requireTypeExport', + line: 10, + column: 27, + endColumn: 28, + data: { + name: 'B', + }, + }, + ], + }, + + { + code: ` + type A = 'test'; + export type B = \`test-\${A}\`; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 3, + column: 33, + endColumn: 34, + data: { + name: 'A', + }, + }, + ], + }, ], }); From 0415b604b6f2cb5f79d260ba86e33855faaf80ca Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 23:02:33 +0300 Subject: [PATCH 61/66] fix false positives due to ordering --- .../src/rules/require-types-exports.ts | 38 ++++++++++++------- .../tests/rules/require-types-exports.test.ts | 12 ++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index 71414d6c5fcf..e17cb7102923 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -36,13 +36,32 @@ export default createRule<[], MessageIds>({ externalizedTypes.add(node.local.name); } - function collectExportedTypes( - node: + function collectExportedTypes(node: TSESTree.Program): void { + const isCollectableType = ( + node: TSESTree.Node, + ): node is | TSESTree.TSTypeAliasDeclaration | TSESTree.TSInterfaceDeclaration - | TSESTree.TSEnumDeclaration, - ): void { - externalizedTypes.add(node.id.name); + | TSESTree.TSEnumDeclaration + | TSESTree.TSModuleDeclaration => { + return [ + AST_NODE_TYPES.TSTypeAliasDeclaration, + AST_NODE_TYPES.TSInterfaceDeclaration, + AST_NODE_TYPES.TSEnumDeclaration, + AST_NODE_TYPES.TSModuleDeclaration, + ].includes(node.type); + }; + + node.body.forEach(statement => { + if ( + statement.type === AST_NODE_TYPES.ExportNamedDeclaration && + statement.declaration && + isCollectableType(statement.declaration) && + statement.declaration.id.type === AST_NODE_TYPES.Identifier + ) { + externalizedTypes.add(statement.declaration.id.name); + } + }); } function visitExportedFunctionDeclaration( @@ -124,14 +143,7 @@ export default createRule<[], MessageIds>({ 'ImportDeclaration ImportNamespaceSpecifier': collectImportedTypes, 'ImportDeclaration ImportDefaultSpecifier': collectImportedTypes, - 'Program > ExportNamedDeclaration > TSTypeAliasDeclaration': - collectExportedTypes, - 'Program > ExportNamedDeclaration > TSInterfaceDeclaration': - collectExportedTypes, - 'Program > ExportNamedDeclaration > TSEnumDeclaration': - collectExportedTypes, - 'Program > ExportNamedDeclaration > TSModuleDeclaration': - collectExportedTypes, + Program: collectExportedTypes, 'ExportNamedDeclaration[declaration.type="FunctionDeclaration"]': visitExportedFunctionDeclaration, diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index b0dde505720b..a465f1fb6b42 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -444,6 +444,18 @@ ruleTester.run('require-types-exports', rule, { func, }; `, + + ` + export function func1() { + return func2(1); + } + + export type A = number; + + export function func2(arg: A) { + return 1; + } + `, ], invalid: [ From a0c236ed38de7a4ea708b54d8723648928147837 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 23:27:11 +0300 Subject: [PATCH 62/66] change message --- packages/eslint-plugin/src/rules/require-types-exports.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index e17cb7102923..ce18c84b698d 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -18,7 +18,8 @@ export default createRule<[], MessageIds>({ recommended: 'strict', }, messages: { - requireTypeExport: 'Expected type "{{ name }}" to be exported', + requireTypeExport: + '"{{ name }}" is used in other exports from this file, so it should also be exported.', }, schema: [], }, From 3d5d695ac54ddcb0cbde86affb3a3ef369b32448 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 23:27:57 +0300 Subject: [PATCH 63/66] wip --- .../src/rules/require-types-exports.ts | 39 +++++++++---------- .../tests/rules/require-types-exports.test.ts | 2 + 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index ce18c84b698d..e9315938ef9b 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -7,7 +7,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, findVariable } from '../util'; -type MessageIds = 'requireTypeExport'; +export type MessageIds = 'requireTypeExport'; export default createRule<[], MessageIds>({ name: 'require-types-exports', @@ -87,20 +87,14 @@ export default createRule<[], MessageIds>({ } } - function visitExportedTypeAliasDeclaration( + function visitExportedTypeDeclaration( node: TSESTree.ExportNamedDeclaration & { - declaration: TSESTree.TSTypeAliasDeclaration; + declaration: + | TSESTree.TSTypeAliasDeclaration + | TSESTree.TSInterfaceDeclaration; }, ): void { - checkNodeTypes(node.declaration.typeAnnotation); - } - - function visitExportedInterfaceDeclaration( - node: TSESTree.ExportNamedDeclaration & { - declaration: TSESTree.TSInterfaceDeclaration; - }, - ): void { - checkNodeTypes(node.declaration.body); + checkNodeTypes(node.declaration); } function visitExportDefaultDeclaration( @@ -162,16 +156,16 @@ export default createRule<[], MessageIds>({ visitExportedVariableDeclaration, 'ExportNamedDeclaration[declaration.type="TSTypeAliasDeclaration"]': - visitExportedTypeAliasDeclaration, + visitExportedTypeDeclaration, 'ExportNamedDeclaration[declaration.type="TSTypeAliasDeclaration"] > ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': - visitExportedTypeAliasDeclaration, + visitExportedTypeDeclaration, 'ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': - visitExportedInterfaceDeclaration, + visitExportedTypeDeclaration, 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"] > ExportNamedDeclaration[declaration.type="TSInterfaceDeclaration"]': - visitExportedInterfaceDeclaration, + visitExportedTypeDeclaration, ExportDefaultDeclaration: visitExportDefaultDeclaration, }; @@ -313,7 +307,8 @@ function getTypeReferencesRecursively( const isBuiltinType = variable instanceof ImplicitLibVariable; const isGenericTypeArg = - variable?.scope.type === ScopeType.function && + (variable?.scope.type === ScopeType.function || + variable?.scope.type === ScopeType.type) && variable.identifiers.every( id => id.parent.type === AST_NODE_TYPES.TSTypeParameter, ); @@ -347,8 +342,12 @@ function getTypeReferencesRecursively( node.types.forEach(collect); break; - case AST_NODE_TYPES.TSInterfaceBody: - node.body.forEach(collect); + case AST_NODE_TYPES.TSTypeAliasDeclaration: + collect(node.typeAnnotation); + break; + + case AST_NODE_TYPES.TSInterfaceDeclaration: + node.body.body.forEach(collect); break; case AST_NODE_TYPES.TSPropertySignature: @@ -402,7 +401,7 @@ function collectFunctionReturnStatements( // Heavily inspired by: // https://github.com/typescript-eslint/typescript-eslint/blob/103de6eed/packages/eslint-plugin/src/util/astUtils.ts#L47-L80 -export function forEachReturnStatement( +function forEachReturnStatement( functionNode: | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index a465f1fb6b42..c5ddc08feb75 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -456,6 +456,8 @@ ruleTester.run('require-types-exports', rule, { return 1; } `, + + 'export type ValueOf = T[keyof T];', ], invalid: [ From 2e76ce6a83f73235ed5104177c8ab557eb21d28f Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 7 Jul 2024 23:49:09 +0300 Subject: [PATCH 64/66] fix some reports --- .../eslint-plugin/src/rules/array-type.ts | 4 +-- .../eslint-plugin/src/rules/ban-ts-comment.ts | 6 ++-- .../src/rules/class-literal-property-style.ts | 4 +-- .../src/rules/class-methods-use-this.ts | 4 +-- .../eslint-plugin/src/rules/comma-spacing.ts | 4 +-- .../rules/consistent-generic-constructors.ts | 4 +-- .../rules/consistent-indexed-object-style.ts | 4 +-- .../src/rules/consistent-return.ts | 6 ++-- .../src/rules/consistent-type-exports.ts | 4 +-- .../src/rules/consistent-type-imports.ts | 8 +++--- .../rules/explicit-function-return-type.ts | 4 +-- .../rules/explicit-member-accessibility.ts | 8 +++--- .../rules/explicit-module-boundary-types.ts | 4 +-- packages/eslint-plugin/src/rules/indent.ts | 4 +-- .../src/rules/lines-between-class-members.ts | 4 +-- .../src/rules/member-delimiter-style.ts | 12 ++++---- .../src/rules/member-ordering.ts | 28 +++++++++---------- .../src/rules/no-array-delete.ts | 2 +- .../src/rules/no-base-to-string.ts | 4 +-- .../src/rules/no-dupe-class-members.ts | 4 +-- .../src/rules/no-empty-function.ts | 4 +-- .../src/rules/no-empty-interface.ts | 4 +-- .../src/rules/no-extra-parens.ts | 4 +-- .../eslint-plugin/src/rules/no-extra-semi.ts | 4 +-- .../src/rules/no-extraneous-class.ts | 4 +-- .../src/rules/no-floating-promises.ts | 4 +-- .../src/rules/no-import-type-side-effects.ts | 4 +-- .../src/rules/no-inferrable-types.ts | 4 +-- .../src/rules/no-invalid-void-type.ts | 4 +-- .../eslint-plugin/src/rules/no-loop-func.ts | 4 +-- .../src/rules/no-loss-of-precision.ts | 6 ++-- .../src/rules/no-magic-numbers.ts | 4 +-- .../src/rules/no-meaningless-void-operator.ts | 2 +- .../src/rules/no-misused-promises.ts | 6 ++-- .../eslint-plugin/src/rules/no-namespace.ts | 4 +-- .../src/rules/no-non-null-assertion.ts | 2 +- .../eslint-plugin/src/rules/no-redeclare.ts | 7 +++-- .../src/rules/no-require-imports.ts | 4 +-- packages/eslint-plugin/src/rules/no-shadow.ts | 4 +-- .../eslint-plugin/src/rules/no-this-alias.ts | 4 +-- .../src/rules/no-throw-literal.ts | 4 +-- .../eslint-plugin/src/rules/no-type-alias.ts | 6 ++-- .../no-unnecessary-boolean-literal-compare.ts | 4 +-- .../no-unnecessary-template-expression.ts | 2 +- .../rules/no-unnecessary-type-arguments.ts | 2 +- .../rules/no-unnecessary-type-assertion.ts | 4 +-- .../src/rules/no-unsafe-argument.ts | 2 +- .../eslint-plugin/src/rules/no-unsafe-call.ts | 2 +- .../src/rules/no-unsafe-unary-minus.ts | 4 +-- .../src/rules/no-unused-expressions.ts | 4 +-- .../src/rules/no-use-before-define.ts | 6 ++-- .../src/rules/no-useless-constructor.ts | 4 +-- .../src/rules/no-useless-template-literals.ts | 2 +- .../src/rules/no-var-requires.ts | 4 +-- .../src/rules/only-throw-error.ts | 4 +-- .../rules/padding-line-between-statements.ts | 6 ++-- .../src/rules/parameter-properties.ts | 8 +++--- .../src/rules/prefer-destructuring.ts | 8 +++--- .../src/rules/prefer-enum-initializers.ts | 2 +- .../compareNodes.ts | 2 +- .../gatherLogicalOperands.ts | 2 +- .../rules/prefer-readonly-parameter-types.ts | 4 +-- .../src/rules/prefer-readonly.ts | 4 +-- .../rules/prefer-string-starts-ends-with.ts | 4 +-- .../src/rules/prefer-ts-expect-error.ts | 2 +- .../src/rules/promise-function-async.ts | 4 +-- .../src/rules/restrict-plus-operands.ts | 4 +-- .../rules/restrict-template-expressions.ts | 4 +-- .../src/rules/space-before-function-paren.ts | 4 +-- .../src/rules/switch-exhaustiveness-check.ts | 4 +-- .../src/rules/triple-slash-reference.ts | 4 +-- .../src/rules/type-annotation-spacing.ts | 4 +-- packages/eslint-plugin/src/rules/typedef.ts | 4 +-- .../eslint-plugin/src/rules/unbound-method.ts | 2 +- .../src/rules/unified-signatures.ts | 4 +-- .../use-unknown-in-catch-callback-variable.ts | 2 +- .../src/util/getESLintCoreRule.ts | 4 +-- .../src/util/getFunctionHeadLoc.ts | 2 +- .../src/util/getOperatorPrecedence.ts | 2 +- .../src/util/getWrappingFixer.ts | 2 +- .../cases/createTestCases.ts | 2 +- .../rules/prefer-optional-chain/base-cases.ts | 4 +-- 82 files changed, 181 insertions(+), 176 deletions(-) diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index 26b5c270914e..ebc32526ee89 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -72,13 +72,13 @@ function typeNeedsParentheses(node: TSESTree.Node): boolean { } export type OptionString = 'array-simple' | 'array' | 'generic'; -type Options = [ +export type Options = [ { default: OptionString; readonly?: OptionString; }, ]; -type MessageIds = +export type MessageIds = | 'errorStringArray' | 'errorStringArraySimple' | 'errorStringGeneric' diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index b554510f57d3..71ec5696f867 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -3,12 +3,12 @@ import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule, getStringLength, nullThrows } from '../util'; -type DirectiveConfig = +export type DirectiveConfig = | boolean | 'allow-with-description' | { descriptionFormat: string }; -interface Options { +export interface Options { 'ts-expect-error'?: DirectiveConfig; 'ts-ignore'?: DirectiveConfig; 'ts-nocheck'?: DirectiveConfig; @@ -18,7 +18,7 @@ interface Options { const defaultMinimumDescriptionLength = 3; -type MessageIds = +export type MessageIds = | 'tsDirectiveComment' | 'tsIgnoreInsteadOfExpectError' | 'tsDirectiveCommentDescriptionNotMatchPattern' diff --git a/packages/eslint-plugin/src/rules/class-literal-property-style.ts b/packages/eslint-plugin/src/rules/class-literal-property-style.ts index 6813e80f60d8..d8aa8d124fe5 100644 --- a/packages/eslint-plugin/src/rules/class-literal-property-style.ts +++ b/packages/eslint-plugin/src/rules/class-literal-property-style.ts @@ -9,8 +9,8 @@ import { nullThrows, } from '../util'; -type Options = ['fields' | 'getters']; -type MessageIds = +export type Options = ['fields' | 'getters']; +export type MessageIds = | 'preferFieldStyle' | 'preferFieldStyleSuggestion' | 'preferGetterStyle' diff --git a/packages/eslint-plugin/src/rules/class-methods-use-this.ts b/packages/eslint-plugin/src/rules/class-methods-use-this.ts index 6236a46ddb7b..793c8e8a5d90 100644 --- a/packages/eslint-plugin/src/rules/class-methods-use-this.ts +++ b/packages/eslint-plugin/src/rules/class-methods-use-this.ts @@ -8,7 +8,7 @@ import { getStaticStringValue, } from '../util'; -type Options = [ +export type Options = [ { exceptMethods?: string[]; enforceForClassFields?: boolean; @@ -16,7 +16,7 @@ type Options = [ ignoreClassesThatImplementAnInterface?: boolean | 'public-fields'; }, ]; -type MessageIds = 'missingThis'; +export type MessageIds = 'missingThis'; export default createRule({ name: 'class-methods-use-this', diff --git a/packages/eslint-plugin/src/rules/comma-spacing.ts b/packages/eslint-plugin/src/rules/comma-spacing.ts index cd283e4c97ac..79e9450c012b 100644 --- a/packages/eslint-plugin/src/rules/comma-spacing.ts +++ b/packages/eslint-plugin/src/rules/comma-spacing.ts @@ -10,13 +10,13 @@ import { isTokenOnSameLine, } from '../util'; -type Options = [ +export type Options = [ { before: boolean; after: boolean; }, ]; -type MessageIds = 'missing' | 'unexpected'; +export type MessageIds = 'missing' | 'unexpected'; export default createRule({ name: 'comma-spacing', diff --git a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts index 166a2a8ecfda..466fc83a544a 100644 --- a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts +++ b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts @@ -3,8 +3,8 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; -type MessageIds = 'preferConstructor' | 'preferTypeAnnotation'; -type Options = ['constructor' | 'type-annotation']; +export type MessageIds = 'preferConstructor' | 'preferTypeAnnotation'; +export type Options = ['constructor' | 'type-annotation']; export default createRule({ name: 'consistent-generic-constructors', diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index f0f91cc32b8e..c85f49f71bf3 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -3,8 +3,8 @@ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type MessageIds = 'preferIndexSignature' | 'preferRecord'; -type Options = ['index-signature' | 'record']; +export type MessageIds = 'preferIndexSignature' | 'preferRecord'; +export type Options = ['index-signature' | 'record']; export default createRule({ name: 'consistent-indexed-object-style', diff --git a/packages/eslint-plugin/src/rules/consistent-return.ts b/packages/eslint-plugin/src/rules/consistent-return.ts index 5d4cc3fb9256..8abb899ef7ce 100644 --- a/packages/eslint-plugin/src/rules/consistent-return.ts +++ b/packages/eslint-plugin/src/rules/consistent-return.ts @@ -11,10 +11,10 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('consistent-return'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; -type FunctionNode = +export type FunctionNode = | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; diff --git a/packages/eslint-plugin/src/rules/consistent-type-exports.ts b/packages/eslint-plugin/src/rules/consistent-type-exports.ts index 236659d13adb..cbd3b3236303 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-exports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-exports.ts @@ -12,7 +12,7 @@ import { NullThrowsReasons, } from '../util'; -type Options = [ +export type Options = [ { fixMixedExportsWithInlineTypeSpecifier: boolean; }, @@ -32,7 +32,7 @@ interface ReportValueExport { inlineTypeSpecifiers: TSESTree.ExportSpecifier[]; } -type MessageIds = +export type MessageIds = | 'multipleExportsAreTypes' | 'singleExportIsType' | 'typeOverValue'; diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts index f4e98d2d9e00..3dfcb8ced4e0 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-imports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -15,10 +15,10 @@ import { NullThrowsReasons, } from '../util'; -type Prefer = 'no-type-imports' | 'type-imports'; -type FixStyle = 'inline-type-imports' | 'separate-type-imports'; +export type Prefer = 'no-type-imports' | 'type-imports'; +export type FixStyle = 'inline-type-imports' | 'separate-type-imports'; -type Options = [ +export type Options = [ { prefer?: Prefer; disallowTypeAnnotations?: boolean; @@ -44,7 +44,7 @@ interface ReportValueImport { inlineTypeSpecifiers: TSESTree.ImportSpecifier[]; } -type MessageIds = +export type MessageIds = | 'typeOverValue' | 'someImportsAreOnlyTypes' | 'avoidImportType' diff --git a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts index 9904dc2e5336..54e8c132c53d 100644 --- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts +++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts @@ -9,7 +9,7 @@ import { isValidFunctionExpressionReturnType, } from '../util/explicitReturnTypeUtils'; -type Options = [ +export type Options = [ { allowExpressions?: boolean; allowTypedFunctionExpressions?: boolean; @@ -21,7 +21,7 @@ type Options = [ allowIIFEs?: boolean; }, ]; -type MessageIds = 'missingReturnType'; +export type MessageIds = 'missingReturnType'; type FunctionNode = | TSESTree.ArrowFunctionExpression diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 5947d292f8bd..2a885d275d37 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -12,12 +12,12 @@ import { getParameterPropertyHeadLoc, } from '../util/getMemberHeadLoc'; -type AccessibilityLevel = +export type AccessibilityLevel = | 'explicit' // require an accessor (including public) | 'no-public' // don't require public | 'off'; // don't check -interface Config { +export interface Config { accessibility?: AccessibilityLevel; ignoredMethodNames?: string[]; overrides?: { @@ -29,9 +29,9 @@ interface Config { }; } -type Options = [Config]; +export type Options = [Config]; -type MessageIds = +export type MessageIds = | 'addExplicitAccessibility' | 'missingAccessibility' | 'unwantedPublicAccessibility'; diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts index c8981e403941..66e606d20538 100644 --- a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -16,7 +16,7 @@ import { isTypedFunctionExpression, } from '../util/explicitReturnTypeUtils'; -type Options = [ +export type Options = [ { allowArgumentsExplicitlyTypedAsAny?: boolean; allowDirectConstAssertionInArrowFunctions?: boolean; @@ -25,7 +25,7 @@ type Options = [ allowTypedFunctionExpressions?: boolean; }, ]; -type MessageIds = +export type MessageIds = | 'anyTypedArg' | 'anyTypedArgUnnamed' | 'missingArgType' diff --git a/packages/eslint-plugin/src/rules/indent.ts b/packages/eslint-plugin/src/rules/indent.ts index 248ecd0d7e8a..11a5d8313a7e 100644 --- a/packages/eslint-plugin/src/rules/indent.ts +++ b/packages/eslint-plugin/src/rules/indent.ts @@ -17,8 +17,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('indent'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; const KNOWN_NODES = new Set([ // Class properties aren't yet supported by eslint... diff --git a/packages/eslint-plugin/src/rules/lines-between-class-members.ts b/packages/eslint-plugin/src/rules/lines-between-class-members.ts index 60da9308757a..2304f7ac405b 100644 --- a/packages/eslint-plugin/src/rules/lines-between-class-members.ts +++ b/packages/eslint-plugin/src/rules/lines-between-class-members.ts @@ -11,8 +11,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('lines-between-class-members'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; const schema = Object.values( deepMerge( diff --git a/packages/eslint-plugin/src/rules/member-delimiter-style.ts b/packages/eslint-plugin/src/rules/member-delimiter-style.ts index 428f31f3667e..13a398defdf7 100644 --- a/packages/eslint-plugin/src/rules/member-delimiter-style.ts +++ b/packages/eslint-plugin/src/rules/member-delimiter-style.ts @@ -4,10 +4,10 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import { createRule, deepMerge } from '../util'; -type Delimiter = 'comma' | 'none' | 'semi'; +export type Delimiter = 'comma' | 'none' | 'semi'; // need type's implicit index sig for deepMerge // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type TypeOptions = { +export type TypeOptions = { delimiter?: Delimiter; requireLast?: boolean; }; @@ -15,19 +15,19 @@ type TypeOptionsWithType = TypeOptions & { type: string; }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type BaseOptions = { +export type BaseOptions = { multiline?: TypeOptions; singleline?: TypeOptions; }; -type Config = BaseOptions & { +export type Config = BaseOptions & { overrides?: { typeLiteral?: BaseOptions; interface?: BaseOptions; }; multilineDetection?: 'brackets' | 'last-member'; }; -type Options = [Config]; -type MessageIds = +export type Options = [Config]; +export type MessageIds = | 'expectedComma' | 'expectedSemi' | 'unexpectedComma' diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 4713192770a7..c357095ae988 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -17,9 +17,9 @@ export type MessageIds = | 'incorrectOrder' | 'incorrectRequiredMembersOrder'; -type ReadonlyType = 'readonly-field' | 'readonly-signature'; +export type ReadonlyType = 'readonly-field' | 'readonly-signature'; -type MemberKind = +export type MemberKind = | ReadonlyType | 'accessor' | 'call-signature' @@ -31,7 +31,7 @@ type MemberKind = | 'signature' | 'static-initialization'; -type DecoratedMemberKind = +export type DecoratedMemberKind = | Exclude | 'accessor' | 'field' @@ -39,16 +39,16 @@ type DecoratedMemberKind = | 'method' | 'set'; -type NonCallableMemberKind = Exclude< +export type NonCallableMemberKind = Exclude< MemberKind, 'constructor' | 'readonly-signature' | 'signature' >; -type MemberScope = 'abstract' | 'instance' | 'static'; +export type MemberScope = 'abstract' | 'instance' | 'static'; -type Accessibility = TSESTree.Accessibility | '#private'; +export type Accessibility = TSESTree.Accessibility | '#private'; -type BaseMemberType = +export type BaseMemberType = | MemberKind | `${Accessibility}-${Exclude< MemberKind, @@ -59,26 +59,26 @@ type BaseMemberType = | `${MemberScope}-${NonCallableMemberKind}` | `decorated-${DecoratedMemberKind}`; -type MemberType = BaseMemberType | BaseMemberType[]; +export type MemberType = BaseMemberType | BaseMemberType[]; -type AlphabeticalOrder = +export type AlphabeticalOrder = | 'alphabetically-case-insensitive' | 'alphabetically' | 'natural-case-insensitive' | 'natural'; -type Order = AlphabeticalOrder | 'as-written'; +export type Order = AlphabeticalOrder | 'as-written'; -interface SortedOrderConfig { +export interface SortedOrderConfig { memberTypes?: MemberType[] | 'never'; optionalityOrder?: OptionalityOrder; order?: Order; } -type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; -type Member = TSESTree.ClassElement | TSESTree.TypeElement; +export type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; +export type Member = TSESTree.ClassElement | TSESTree.TypeElement; -type OptionalityOrder = 'optional-first' | 'required-first'; +export type OptionalityOrder = 'optional-first' | 'required-first'; export type Options = [ { diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index d9900a1df10f..96bd2fdc2568 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -8,7 +8,7 @@ import { getParserServices, } from '../util'; -type MessageId = 'noArrayDelete' | 'useSplice'; +export type MessageId = 'noArrayDelete' | 'useSplice'; export default createRule<[], MessageId>({ name: 'no-array-delete', diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 0369e66fe66f..0dee231a011f 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -10,12 +10,12 @@ enum Usefulness { Sometimes = 'may', } -type Options = [ +export type Options = [ { ignoredTypeNames?: string[]; }, ]; -type MessageIds = 'baseToString'; +export type MessageIds = 'baseToString'; export default createRule({ name: 'no-base-to-string', diff --git a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts index 08dd0b35d3b8..e1ffecdd6dad 100644 --- a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts @@ -10,8 +10,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-dupe-class-members'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ name: 'no-dupe-class-members', diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index 6a8e90ebaf13..79e6ffd9cc56 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -11,8 +11,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-empty-function'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; const schema = deepMerge( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- https://github.com/microsoft/TypeScript/issues/17002 diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts index 674b86d44bf9..0e56604d2567 100644 --- a/packages/eslint-plugin/src/rules/no-empty-interface.ts +++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts @@ -4,12 +4,12 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isDefinitionFile } from '../util'; -type Options = [ +export type Options = [ { allowSingleExtends?: boolean; }, ]; -type MessageIds = 'noEmpty' | 'noEmptyWithSuper'; +export type MessageIds = 'noEmpty' | 'noEmptyWithSuper'; export default createRule({ name: 'no-empty-interface', diff --git a/packages/eslint-plugin/src/rules/no-extra-parens.ts b/packages/eslint-plugin/src/rules/no-extra-parens.ts index 0ed0f4c6b4de..bd1dde121c3f 100644 --- a/packages/eslint-plugin/src/rules/no-extra-parens.ts +++ b/packages/eslint-plugin/src/rules/no-extra-parens.ts @@ -13,8 +13,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-extra-parens'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ name: 'no-extra-parens', diff --git a/packages/eslint-plugin/src/rules/no-extra-semi.ts b/packages/eslint-plugin/src/rules/no-extra-semi.ts index 4d68f2d6db46..d384cde41fc8 100644 --- a/packages/eslint-plugin/src/rules/no-extra-semi.ts +++ b/packages/eslint-plugin/src/rules/no-extra-semi.ts @@ -7,8 +7,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-extra-semi'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ name: 'no-extra-semi', diff --git a/packages/eslint-plugin/src/rules/no-extraneous-class.ts b/packages/eslint-plugin/src/rules/no-extraneous-class.ts index a7c05f8cc5a8..6d975a454b9a 100644 --- a/packages/eslint-plugin/src/rules/no-extraneous-class.ts +++ b/packages/eslint-plugin/src/rules/no-extraneous-class.ts @@ -3,7 +3,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type Options = [ +export type Options = [ { allowConstructorOnly?: boolean; allowEmpty?: boolean; @@ -11,7 +11,7 @@ type Options = [ allowWithDecorator?: boolean; }, ]; -type MessageIds = 'empty' | 'onlyConstructor' | 'onlyStatic'; +export type MessageIds = 'empty' | 'onlyConstructor' | 'onlyStatic'; export default createRule({ name: 'no-extraneous-class', diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 1ae5e602ae0e..8bbc237803af 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -14,7 +14,7 @@ import { typeMatchesSpecifier, } from '../util'; -type Options = [ +export type Options = [ { ignoreVoid?: boolean; ignoreIIFE?: boolean; @@ -22,7 +22,7 @@ type Options = [ }, ]; -type MessageId = +export type MessageId = | 'floating' | 'floatingVoid' | 'floatingUselessRejectionHandler' diff --git a/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts b/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts index 1658d471bc7b..585e78a177de 100644 --- a/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts +++ b/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts @@ -9,8 +9,8 @@ import { NullThrowsReasons, } from '../util'; -type Options = []; -type MessageIds = 'useTopLevelQualifier'; +export type Options = []; +export type MessageIds = 'useTopLevelQualifier'; export default createRule({ name: 'no-import-type-side-effects', diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts index 51ead2ae476a..3b76ee94b0c6 100644 --- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts +++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts @@ -4,13 +4,13 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; -type Options = [ +export type Options = [ { ignoreParameters?: boolean; ignoreProperties?: boolean; }, ]; -type MessageIds = 'noInferrableType'; +export type MessageIds = 'noInferrableType'; export default createRule({ name: 'no-inferrable-types', diff --git a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts index 8ae1604a210b..4d4189ac7dfc 100644 --- a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts +++ b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts @@ -3,12 +3,12 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -interface Options { +export interface Options { allowInGenericTypeArguments?: [string, ...string[]] | boolean; allowAsThisParameter?: boolean; } -type MessageIds = +export type MessageIds = | 'invalidVoidForGeneric' | 'invalidVoidNotReturn' | 'invalidVoidNotReturnOrGeneric' diff --git a/packages/eslint-plugin/src/rules/no-loop-func.ts b/packages/eslint-plugin/src/rules/no-loop-func.ts index a34217453b0d..096e3618f5d4 100644 --- a/packages/eslint-plugin/src/rules/no-loop-func.ts +++ b/packages/eslint-plugin/src/rules/no-loop-func.ts @@ -10,8 +10,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-loop-func'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ name: 'no-loop-func', diff --git a/packages/eslint-plugin/src/rules/no-loss-of-precision.ts b/packages/eslint-plugin/src/rules/no-loss-of-precision.ts index 06938f9e4dcf..51d2bdee9f25 100644 --- a/packages/eslint-plugin/src/rules/no-loss-of-precision.ts +++ b/packages/eslint-plugin/src/rules/no-loss-of-precision.ts @@ -9,8 +9,10 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-loss-of-precision'); -type Options = InferOptionsTypeFromRule>; -type MessageIds = InferMessageIdsTypeFromRule>; +export type Options = InferOptionsTypeFromRule>; +export type MessageIds = InferMessageIdsTypeFromRule< + NonNullable +>; export default createRule({ name: 'no-loss-of-precision', diff --git a/packages/eslint-plugin/src/rules/no-magic-numbers.ts b/packages/eslint-plugin/src/rules/no-magic-numbers.ts index 5a86056cb90d..d31cdd895e36 100644 --- a/packages/eslint-plugin/src/rules/no-magic-numbers.ts +++ b/packages/eslint-plugin/src/rules/no-magic-numbers.ts @@ -11,8 +11,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-magic-numbers'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; // Extend base schema with additional property to ignore TS numeric literal types const schema = deepMerge( diff --git a/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts b/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts index e8fdde8d1f33..d1d21612476c 100644 --- a/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts +++ b/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; import { createRule } from '../util'; -type Options = [ +export type Options = [ { checkNever: boolean; }, diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index 1a58d884dc7c..2c386402a162 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -9,7 +9,7 @@ import { isRestParameterDeclaration, } from '../util'; -type Options = [ +export type Options = [ { checksConditionals?: boolean; checksVoidReturn?: ChecksVoidReturnOptions | boolean; @@ -17,7 +17,7 @@ type Options = [ }, ]; -interface ChecksVoidReturnOptions { +export interface ChecksVoidReturnOptions { arguments?: boolean; attributes?: boolean; properties?: boolean; @@ -25,7 +25,7 @@ interface ChecksVoidReturnOptions { variables?: boolean; } -type MessageId = +export type MessageId = | 'conditional' | 'spread' | 'voidReturnArgument' diff --git a/packages/eslint-plugin/src/rules/no-namespace.ts b/packages/eslint-plugin/src/rules/no-namespace.ts index c6b9213259be..67a4979358ba 100644 --- a/packages/eslint-plugin/src/rules/no-namespace.ts +++ b/packages/eslint-plugin/src/rules/no-namespace.ts @@ -3,13 +3,13 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isDefinitionFile } from '../util'; -type Options = [ +export type Options = [ { allowDeclarations?: boolean; allowDefinitionFiles?: boolean; }, ]; -type MessageIds = 'moduleSyntaxIsPreferred'; +export type MessageIds = 'moduleSyntaxIsPreferred'; export default createRule({ name: 'no-namespace', diff --git a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts index 8545b0e1c110..f34637eeb42d 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts @@ -8,7 +8,7 @@ import { NullThrowsReasons, } from '../util'; -type MessageIds = 'noNonNull' | 'suggestOptionalChain'; +export type MessageIds = 'noNonNull' | 'suggestOptionalChain'; export default createRule<[], MessageIds>({ name: 'no-non-null-assertion', diff --git a/packages/eslint-plugin/src/rules/no-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts index b084d90650b0..782df71aadbc 100644 --- a/packages/eslint-plugin/src/rules/no-redeclare.ts +++ b/packages/eslint-plugin/src/rules/no-redeclare.ts @@ -4,8 +4,11 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, getNameLocationInGlobalDirectiveComment } from '../util'; -type MessageIds = 'redeclared' | 'redeclaredAsBuiltin' | 'redeclaredBySyntax'; -type Options = [ +export type MessageIds = + | 'redeclared' + | 'redeclaredAsBuiltin' + | 'redeclaredBySyntax'; +export type Options = [ { builtinGlobals?: boolean; ignoreDeclarationMerge?: boolean; diff --git a/packages/eslint-plugin/src/rules/no-require-imports.ts b/packages/eslint-plugin/src/rules/no-require-imports.ts index 1956694484f4..577b36371905 100644 --- a/packages/eslint-plugin/src/rules/no-require-imports.ts +++ b/packages/eslint-plugin/src/rules/no-require-imports.ts @@ -3,12 +3,12 @@ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import * as util from '../util'; -type Options = [ +export type Options = [ { allow: string[]; }, ]; -type MessageIds = 'noRequireImports'; +export type MessageIds = 'noRequireImports'; export default util.createRule({ name: 'no-require-imports', diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index f66c21f6cdb1..78b59c53c775 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -8,8 +8,8 @@ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type MessageIds = 'noShadow' | 'noShadowGlobal'; -type Options = [ +export type MessageIds = 'noShadow' | 'noShadowGlobal'; +export type Options = [ { allow?: string[]; builtinGlobals?: boolean; diff --git a/packages/eslint-plugin/src/rules/no-this-alias.ts b/packages/eslint-plugin/src/rules/no-this-alias.ts index 98006e56328d..5ddbc7463bf0 100644 --- a/packages/eslint-plugin/src/rules/no-this-alias.ts +++ b/packages/eslint-plugin/src/rules/no-this-alias.ts @@ -3,13 +3,13 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type Options = [ +export type Options = [ { allowDestructuring?: boolean; allowedNames?: string[]; }, ]; -type MessageIds = 'thisAssignment' | 'thisDestructure'; +export type MessageIds = 'thisAssignment' | 'thisDestructure'; export default createRule({ name: 'no-this-alias', diff --git a/packages/eslint-plugin/src/rules/no-throw-literal.ts b/packages/eslint-plugin/src/rules/no-throw-literal.ts index d893dbc164a5..2318c75b742e 100644 --- a/packages/eslint-plugin/src/rules/no-throw-literal.ts +++ b/packages/eslint-plugin/src/rules/no-throw-literal.ts @@ -10,9 +10,9 @@ import { isTypeUnknownType, } from '../util'; -type MessageIds = 'object' | 'undef'; +export type MessageIds = 'object' | 'undef'; -type Options = [ +export type Options = [ { allowThrowingAny?: boolean; allowThrowingUnknown?: boolean; diff --git a/packages/eslint-plugin/src/rules/no-type-alias.ts b/packages/eslint-plugin/src/rules/no-type-alias.ts index 53e64a3b5c69..cd7898f7e9c4 100644 --- a/packages/eslint-plugin/src/rules/no-type-alias.ts +++ b/packages/eslint-plugin/src/rules/no-type-alias.ts @@ -3,14 +3,14 @@ import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type Values = +export type Values = | 'always' | 'in-intersections' | 'in-unions-and-intersections' | 'in-unions' | 'never'; -type Options = [ +export type Options = [ { allowAliases?: Values; allowCallbacks?: 'always' | 'never'; @@ -22,7 +22,7 @@ type Options = [ allowGenerics?: 'always' | 'never'; }, ]; -type MessageIds = 'noCompositionAlias' | 'noTypeAlias'; +export type MessageIds = 'noCompositionAlias' | 'noTypeAlias'; type CompositionType = | AST_NODE_TYPES.TSIntersectionType diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts index b472f75e5a0a..9900c20ed22e 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts @@ -5,14 +5,14 @@ import * as ts from 'typescript'; import { createRule, getParserServices, isStrongPrecedenceNode } from '../util'; -type MessageIds = +export type MessageIds = | 'comparingNullableToFalse' | 'comparingNullableToTrueDirect' | 'comparingNullableToTrueNegated' | 'direct' | 'negated'; -type Options = [ +export type Options = [ { allowComparingNullableBooleansToTrue?: boolean; allowComparingNullableBooleansToFalse?: boolean; diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index 44e10c5e33c8..aa35bda65f44 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -11,7 +11,7 @@ import { isUndefinedIdentifier, } from '../util'; -type MessageId = 'noUnnecessaryTemplateExpression'; +export type MessageId = 'noUnnecessaryTemplateExpression'; export default createRule<[], MessageId>({ name: 'no-unnecessary-template-expression', diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index d482ef1b672a..711565a11d96 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -20,7 +20,7 @@ type ParameterCapableTSNode = | ts.TypeQueryNode | ts.TypeReferenceNode; -type MessageIds = 'unnecessaryTypeParameter'; +export type MessageIds = 'unnecessaryTypeParameter'; export default createRule<[], MessageIds>({ name: 'no-unnecessary-type-arguments', diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 69f5daaad619..b128178b6bc1 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -15,12 +15,12 @@ import { NullThrowsReasons, } from '../util'; -type Options = [ +export type Options = [ { typesToIgnore?: string[]; }, ]; -type MessageIds = 'contextuallyUnnecessary' | 'unnecessaryAssertion'; +export type MessageIds = 'contextuallyUnnecessary' | 'unnecessaryAssertion'; export default createRule({ name: 'no-unnecessary-type-assertion', diff --git a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts index 08950fa8d732..3b03af48065b 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts @@ -12,7 +12,7 @@ import { nullThrows, } from '../util'; -type MessageIds = +export type MessageIds = | 'unsafeArgument' | 'unsafeArraySpread' | 'unsafeSpread' diff --git a/packages/eslint-plugin/src/rules/no-unsafe-call.ts b/packages/eslint-plugin/src/rules/no-unsafe-call.ts index b4ec6379f2e1..7be407630cf9 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-call.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts @@ -9,7 +9,7 @@ import { isTypeAnyType, } from '../util'; -type MessageIds = +export type MessageIds = | 'unsafeCall' | 'unsafeCallThis' | 'unsafeNew' diff --git a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts index 66488e37124a..a95633404d1d 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -3,8 +3,8 @@ import * as ts from 'typescript'; import * as util from '../util'; -type Options = []; -type MessageIds = 'unaryMinus'; +export type Options = []; +export type MessageIds = 'unaryMinus'; export default util.createRule({ name: 'no-unsafe-unary-minus', diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index 83c2ddd6c527..aab3bbf4f5b5 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -9,8 +9,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-unused-expressions'); -type MessageIds = InferMessageIdsTypeFromRule; -type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; export default createRule({ name: 'no-unused-expressions', diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts index d577773de9ef..551bfe9ecc39 100644 --- a/packages/eslint-plugin/src/rules/no-use-before-define.ts +++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts @@ -219,7 +219,7 @@ function isInInitializer( return false; } -interface Config { +export interface Config { functions?: boolean; classes?: boolean; enums?: boolean; @@ -228,8 +228,8 @@ interface Config { ignoreTypeReferences?: boolean; allowNamedExports?: boolean; } -type Options = [Config | 'nofunc']; -type MessageIds = 'noUseBeforeDefine'; +export type Options = [Config | 'nofunc']; +export type MessageIds = 'noUseBeforeDefine'; export default createRule({ name: 'no-use-before-define', diff --git a/packages/eslint-plugin/src/rules/no-useless-constructor.ts b/packages/eslint-plugin/src/rules/no-useless-constructor.ts index dcfc7dd976de..460132f0cb33 100644 --- a/packages/eslint-plugin/src/rules/no-useless-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-useless-constructor.ts @@ -10,8 +10,8 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('no-useless-constructor'); -type Options = InferOptionsTypeFromRule; -type MessageIds = InferMessageIdsTypeFromRule; +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; /** * Check if method with accessibility is not useless diff --git a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts index 7b13cd8e2e9a..3dd793e79831 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -11,7 +11,7 @@ import { isUndefinedIdentifier, } from '../util'; -type MessageId = 'noUnnecessaryTemplateExpression'; +export type MessageId = 'noUnnecessaryTemplateExpression'; export default createRule<[], MessageId>({ name: 'no-useless-template-literals', diff --git a/packages/eslint-plugin/src/rules/no-var-requires.ts b/packages/eslint-plugin/src/rules/no-var-requires.ts index 9bb9fb7c1921..ec8e00078360 100644 --- a/packages/eslint-plugin/src/rules/no-var-requires.ts +++ b/packages/eslint-plugin/src/rules/no-var-requires.ts @@ -3,12 +3,12 @@ import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule, getStaticStringValue } from '../util'; -type Options = [ +export type Options = [ { allow: string[]; }, ]; -type MessageIds = 'noVarReqs'; +export type MessageIds = 'noVarReqs'; export default createRule({ name: 'no-var-requires', diff --git a/packages/eslint-plugin/src/rules/only-throw-error.ts b/packages/eslint-plugin/src/rules/only-throw-error.ts index 62ce268fc700..bc30a31b1428 100644 --- a/packages/eslint-plugin/src/rules/only-throw-error.ts +++ b/packages/eslint-plugin/src/rules/only-throw-error.ts @@ -10,9 +10,9 @@ import { isTypeUnknownType, } from '../util'; -type MessageIds = 'object' | 'undef'; +export type MessageIds = 'object' | 'undef'; -type Options = [ +export type Options = [ { allowThrowingAny?: boolean; allowThrowingUnknown?: boolean; diff --git a/packages/eslint-plugin/src/rules/padding-line-between-statements.ts b/packages/eslint-plugin/src/rules/padding-line-between-statements.ts index ecd87a06a643..be4227903475 100644 --- a/packages/eslint-plugin/src/rules/padding-line-between-statements.ts +++ b/packages/eslint-plugin/src/rules/padding-line-between-statements.ts @@ -35,14 +35,14 @@ interface NodeTestObject { test: NodeTest; } -interface PaddingOption { +export interface PaddingOption { blankLine: keyof typeof PaddingTypes; prev: string[] | string; next: string[] | string; } -type MessageIds = 'expectedBlankLine' | 'unexpectedBlankLine'; -type Options = PaddingOption[]; +export type MessageIds = 'expectedBlankLine' | 'unexpectedBlankLine'; +export type Options = PaddingOption[]; const LT = `[${Array.from( new Set(['\r\n', '\r', '\n', '\u2028', '\u2029']), diff --git a/packages/eslint-plugin/src/rules/parameter-properties.ts b/packages/eslint-plugin/src/rules/parameter-properties.ts index 5246594e912e..91408e162d37 100644 --- a/packages/eslint-plugin/src/rules/parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/parameter-properties.ts @@ -3,7 +3,7 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows } from '../util'; -type Modifier = +export type Modifier = | 'private readonly' | 'private' | 'protected readonly' @@ -12,16 +12,16 @@ type Modifier = | 'public' | 'readonly'; -type Prefer = 'class-property' | 'parameter-property'; +export type Prefer = 'class-property' | 'parameter-property'; -type Options = [ +export type Options = [ { allow?: Modifier[]; prefer?: Prefer; }, ]; -type MessageIds = 'preferClassProperty' | 'preferParameterProperty'; +export type MessageIds = 'preferClassProperty' | 'preferParameterProperty'; export default createRule({ name: 'parameter-properties', diff --git a/packages/eslint-plugin/src/rules/prefer-destructuring.ts b/packages/eslint-plugin/src/rules/prefer-destructuring.ts index 60e53dbb61e6..a50f1f8fac46 100644 --- a/packages/eslint-plugin/src/rules/prefer-destructuring.ts +++ b/packages/eslint-plugin/src/rules/prefer-destructuring.ts @@ -13,13 +13,13 @@ import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('prefer-destructuring'); -type BaseOptions = InferOptionsTypeFromRule; -type EnforcementOptions = BaseOptions[1] & { +export type BaseOptions = InferOptionsTypeFromRule; +export type EnforcementOptions = BaseOptions[1] & { enforceForDeclarationWithTypeAnnotation?: boolean; }; -type Options = [BaseOptions[0], EnforcementOptions]; +export type Options = [BaseOptions[0], EnforcementOptions]; -type MessageIds = InferMessageIdsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; const destructuringTypeConfig: JSONSchema4 = { type: 'object', diff --git a/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts index 27572b4f8f7f..9ef9191cfa7d 100644 --- a/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts +++ b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts @@ -2,7 +2,7 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type MessageIds = 'defineInitializer' | 'defineInitializerSuggestion'; +export type MessageIds = 'defineInitializer' | 'defineInitializerSuggestion'; export default createRule<[], MessageIds>({ name: 'prefer-enum-initializers', diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts index d9dce486ec91..7d6acc5671f8 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts @@ -119,7 +119,7 @@ function compareByVisiting( return NodeComparisonResult.Equal; } -type CompareNodesArgument = TSESTree.Node | null | undefined; +export type CompareNodesArgument = TSESTree.Node | null | undefined; function compareNodesUncached( nodeA: TSESTree.Node, nodeB: TSESTree.Node, diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 54c44f4edda9..dd7beac839a8 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -56,7 +56,7 @@ export interface ValidOperand { export interface InvalidOperand { type: OperandValidity.Invalid; } -type Operand = ValidOperand | InvalidOperand; +export type Operand = ValidOperand | InvalidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( diff --git a/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts index 6cb935c1db9a..9329d2c757fc 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts @@ -10,7 +10,7 @@ import { readonlynessOptionsSchema, } from '../util'; -type Options = [ +export type Options = [ { allow?: TypeOrValueSpecifier[]; checkParameterProperties?: boolean; @@ -18,7 +18,7 @@ type Options = [ treatMethodsAsReadonly?: boolean; }, ]; -type MessageIds = 'shouldBeReadonly'; +export type MessageIds = 'shouldBeReadonly'; export default createRule({ name: 'prefer-readonly-parameter-types', diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index f163b1aaab34..02adbc4640d0 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -14,8 +14,8 @@ import { getParameterPropertyHeadLoc, } from '../util/getMemberHeadLoc'; -type MessageIds = 'preferReadonly'; -type Options = [ +export type MessageIds = 'preferReadonly'; +export type Options = [ { onlyInlineLambdas?: boolean; }, diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index f6a22152043c..ac139d381e77 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -16,7 +16,7 @@ import { const EQ_OPERATORS = /^[=!]=/; const regexpp = new RegExpParser(); -type AllowedSingleElementEquality = 'always' | 'never'; +export type AllowedSingleElementEquality = 'always' | 'never'; export type Options = [ { @@ -24,7 +24,7 @@ export type Options = [ }, ]; -type MessageIds = 'preferEndsWith' | 'preferStartsWith'; +export type MessageIds = 'preferEndsWith' | 'preferStartsWith'; export default createRule({ name: 'prefer-string-starts-ends-with', diff --git a/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts b/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts index 6ae1f11720e1..4a6851686383 100644 --- a/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts +++ b/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts @@ -4,7 +4,7 @@ import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; import { createRule } from '../util'; -type MessageIds = 'preferExpectErrorComment'; +export type MessageIds = 'preferExpectErrorComment'; export default createRule<[], MessageIds>({ name: 'prefer-ts-expect-error', diff --git a/packages/eslint-plugin/src/rules/promise-function-async.ts b/packages/eslint-plugin/src/rules/promise-function-async.ts index c191ce45b0db..3cb6a8c9b582 100644 --- a/packages/eslint-plugin/src/rules/promise-function-async.ts +++ b/packages/eslint-plugin/src/rules/promise-function-async.ts @@ -12,7 +12,7 @@ import { NullThrowsReasons, } from '../util'; -type Options = [ +export type Options = [ { allowAny?: boolean; allowedPromiseNames?: string[]; @@ -22,7 +22,7 @@ type Options = [ checkMethodDeclarations?: boolean; }, ]; -type MessageIds = 'missingAsync'; +export type MessageIds = 'missingAsync'; export default createRule({ name: 'promise-function-async', diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index 1953c15c70cb..f70603498d8f 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -11,7 +11,7 @@ import { isTypeFlagSet, } from '../util'; -type Options = [ +export type Options = [ { allowAny?: boolean; allowBoolean?: boolean; @@ -22,7 +22,7 @@ type Options = [ }, ]; -type MessageIds = 'bigintAndNumber' | 'invalid' | 'mismatched'; +export type MessageIds = 'bigintAndNumber' | 'invalid' | 'mismatched'; export default createRule({ name: 'restrict-plus-operands', diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index cc719fe7fb7b..3447ddab7b3c 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -49,11 +49,11 @@ const optionTesters = ( option: `allow${type}` as const, tester, })); -type Options = [ +export type Options = [ { [Type in (typeof optionTesters)[number]['option']]?: boolean }, ]; -type MessageId = 'invalidType'; +export type MessageId = 'invalidType'; export default createRule({ name: 'restrict-template-expressions', diff --git a/packages/eslint-plugin/src/rules/space-before-function-paren.ts b/packages/eslint-plugin/src/rules/space-before-function-paren.ts index 78ea98239db7..3d8c813ccc46 100644 --- a/packages/eslint-plugin/src/rules/space-before-function-paren.ts +++ b/packages/eslint-plugin/src/rules/space-before-function-paren.ts @@ -4,8 +4,8 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isOpeningParenToken } from '../util'; -type Option = 'always' | 'never'; -type FuncOption = Option | 'ignore'; +export type Option = 'always' | 'never'; +export type FuncOption = Option | 'ignore'; export type Options = [ | Option diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 1452d7de6cdb..e9a784c6374d 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -20,7 +20,7 @@ interface SwitchMetadata { readonly containsNonLiteralType: boolean; } -type Options = [ +export type Options = [ { /** * If `true`, allow `default` cases on switch statements with exhaustive @@ -39,7 +39,7 @@ type Options = [ }, ]; -type MessageIds = +export type MessageIds = | 'switchIsNotExhaustive' | 'dangerousDefaultCase' | 'addMissingCases'; diff --git a/packages/eslint-plugin/src/rules/triple-slash-reference.ts b/packages/eslint-plugin/src/rules/triple-slash-reference.ts index a45662c33d4c..7bec0ae018bf 100644 --- a/packages/eslint-plugin/src/rules/triple-slash-reference.ts +++ b/packages/eslint-plugin/src/rules/triple-slash-reference.ts @@ -3,14 +3,14 @@ import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; -type Options = [ +export type Options = [ { lib?: 'always' | 'never'; path?: 'always' | 'never'; types?: 'always' | 'never' | 'prefer-import'; }, ]; -type MessageIds = 'tripleSlashReference'; +export type MessageIds = 'tripleSlashReference'; export default createRule({ name: 'triple-slash-reference', diff --git a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts index 47fbf80d8ac0..0f0988a3332b 100644 --- a/packages/eslint-plugin/src/rules/type-annotation-spacing.ts +++ b/packages/eslint-plugin/src/rules/type-annotation-spacing.ts @@ -31,8 +31,8 @@ interface Config extends WhitespaceRule { type WhitespaceRules = Required; -type Options = [Config?]; -type MessageIds = +export type Options = [Config?]; +export type MessageIds = | 'expectedSpaceAfter' | 'expectedSpaceBefore' | 'unexpectedSpaceAfter' diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 187f4d620365..690386f2c9ce 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -14,9 +14,9 @@ const enum OptionKeys { VariableDeclarationIgnoreFunction = 'variableDeclarationIgnoreFunction', } -type Options = { [k in OptionKeys]?: boolean }; +export type Options = { [k in OptionKeys]?: boolean }; -type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; +export type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; export default createRule<[Options], MessageIds>({ name: 'typedef', diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 9b416ea4a570..6a87b9ea6f78 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -14,7 +14,7 @@ import { // Rule Definition //------------------------------------------------------------------------------ -interface Config { +export interface Config { ignoreStatic: boolean; } diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index c8fa2f85f7df..14db3f84df78 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -51,12 +51,12 @@ type MethodDefinition = | TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition; -type MessageIds = +export type MessageIds = | 'omittingRestParameter' | 'omittingSingleParameter' | 'singleParameterDifference'; -type Options = [ +export type Options = [ { ignoreDifferentlyNamedParameters?: boolean; }, diff --git a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts index b899b23c391d..74d09f305701 100644 --- a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts +++ b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts @@ -16,7 +16,7 @@ import { nullThrows, } from '../util'; -type MessageIds = +export type MessageIds = | 'useUnknown' | 'useUnknownSpreadArgs' | 'useUnknownArrayDestructuringPattern' diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index be28069d2877..761d7e8d0f82 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -1,7 +1,7 @@ import { ESLintUtils } from '@typescript-eslint/utils'; import { builtinRules } from 'eslint/use-at-your-own-risk'; -interface RuleMap { +export interface RuleMap { /* eslint-disable @typescript-eslint/consistent-type-imports -- more concise to use inline imports */ 'arrow-parens': typeof import('eslint/lib/rules/arrow-parens'); 'block-spacing': typeof import('eslint/lib/rules/block-spacing'); @@ -42,7 +42,7 @@ interface RuleMap { /* eslint-enable @typescript-eslint/consistent-type-imports */ } -type RuleId = keyof RuleMap; +export type RuleId = keyof RuleMap; export const getESLintCoreRule = (ruleId: R): RuleMap[R] => ESLintUtils.nullThrows( diff --git a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts index a8f6bc40afbd..f1aa6c9ce68c 100644 --- a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts @@ -5,7 +5,7 @@ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; import { isArrowToken, isOpeningParenToken } from './astUtils'; -type FunctionNode = +export type FunctionNode = | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression; diff --git a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts index 8aac3d6fbd2d..d14c05e963a6 100644 --- a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts +++ b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts @@ -295,7 +295,7 @@ export function getOperatorPrecedenceForNode( } } -type TSESTreeOperatorKind = +export type TSESTreeOperatorKind = | ValueOf | ValueOf; diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index d5d07b6ba7e1..4c95b7fd4db4 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -5,7 +5,7 @@ import { ESLintUtils, } from '@typescript-eslint/utils'; -interface WrappingFixerParams { +export interface WrappingFixerParams { /** Source code. */ sourceCode: Readonly; /** The node we want to modify. */ diff --git a/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts b/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts index 7e1207a222d2..90229443cf7c 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts @@ -77,7 +77,7 @@ const IGNORED_FILTER = { regex: /.gnored/.source, }; -type Cases = { code: string[]; options: Omit }[]; +export type Cases = { code: string[]; options: Omit }[]; export function createTestCases(cases: Cases): void { const createValidTestCases = (): TSESLint.ValidTestCase[] => diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/base-cases.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/base-cases.ts index 28b75a91697d..d61a5556ccca 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/base-cases.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/base-cases.ts @@ -5,8 +5,8 @@ import type { PreferOptionalChainOptions, } from '../../../src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions'; -type MutateFn = (c: string) => string; -type BaseCaseCreator = (args: { +export type MutateFn = (c: string) => string; +export type BaseCaseCreator = (args: { operator: '&&' | '||'; mutateCode?: MutateFn; mutateOutput?: MutateFn; From 88713cb2097c3c2fffc8cc50097d64dbef56883b Mon Sep 17 00:00:00 2001 From: StyleShit Date: Mon, 8 Jul 2024 08:21:00 +0300 Subject: [PATCH 65/66] support keyof & typeof --- .../src/rules/require-types-exports.ts | 69 +++++++++++++-- .../tests/rules/require-types-exports.test.ts | 84 +++++++++++++++++++ 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index e9315938ef9b..222bdeb1edc7 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -104,15 +104,16 @@ export default createRule<[], MessageIds>({ } function checkNodeTypes(node: TSESTree.Node): void { - const typeReferences = getTypeReferencesRecursively( + const { typeReferences, typeQueries } = getVisibleTypesRecursively( node, context.sourceCode, ); - typeReferences.forEach(checkTypeNode); + typeReferences.forEach(checkTypeReference); + typeQueries.forEach(checkTypeQuery); } - function checkTypeNode(node: TSESTree.TSTypeReference): void { + function checkTypeReference(node: TSESTree.TSTypeReference): void { const name = getTypeName(node.typeName); const isExternalized = externalizedTypes.has(name); @@ -123,7 +124,25 @@ export default createRule<[], MessageIds>({ } context.report({ - node: node, + node, + messageId: 'requireTypeExport', + data: { + name, + }, + }); + + reportedTypes.add(name); + } + + function checkTypeQuery(node: TSESTree.TSTypeQuery): void { + const name = context.sourceCode.getText(node); + const isReported = reportedTypes.has(name); + + if (isReported) { + return; + } + context.report({ + node, messageId: 'requireTypeExport', data: { name, @@ -187,11 +206,15 @@ function getTypeName(typeName: TSESTree.EntityName): string { } } -function getTypeReferencesRecursively( +function getVisibleTypesRecursively( node: TSESTree.Node, sourceCode: TSESLint.SourceCode, -): Set { +): { + typeReferences: Set; + typeQueries: Set; +} { const typeReferences = new Set(); + const typeQueries = new Set(); const visited = new Set(); collect(node); @@ -321,6 +344,16 @@ function getTypeReferencesRecursively( break; } + case AST_NODE_TYPES.TSTypeOperator: + collect(node.typeAnnotation); + break; + + case AST_NODE_TYPES.TSTypeQuery: + if (isInsideFunctionDeclaration(node)) { + typeQueries.add(node); + } + break; + case AST_NODE_TYPES.TSArrayType: collect(node.elementType); break; @@ -373,7 +406,29 @@ function getTypeReferencesRecursively( } } - return typeReferences; + return { + typeReferences, + typeQueries, + }; +} + +function isInsideFunctionDeclaration(node: TSESTree.Node): boolean { + const functionNodes = new Set([ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.TSDeclareFunction, + ]); + + if (!node.parent) { + return false; + } + + if (functionNodes.has(node.parent.type)) { + return true; + } + + return isInsideFunctionDeclaration(node.parent); } function collectFunctionReturnStatements( diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index c5ddc08feb75..5629431c0711 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -458,6 +458,25 @@ ruleTester.run('require-types-exports', rule, { `, 'export type ValueOf = T[keyof T];', + + ` + const fruits = { apple: 'apple' }; + export type Fruits = typeof fruits; + + export function getFruit(key: Key): Fruits[Key] { + return fruits[key]; + } + `, + + ` + const fruits = { apple: 'apple' }; + + export function doWork(): number { + const fruit: keyof typeof fruits = 'apple'; + + return 1; + } + `, ], invalid: [ @@ -3405,5 +3424,70 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + const fruits = { apple: 'apple' }; + + export function getFruit( + key: Key, + ): (typeof fruits)[Key] { + return fruits[key]; + } + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 52, + endColumn: 65, + data: { + name: 'typeof fruits', + }, + }, + ], + }, + + { + code: ` + const fruits = { apple: 'apple' }; + + export declare function processFruit( + fruit: F, + ): void; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 4, + column: 56, + endColumn: 75, + data: { + name: 'typeof fruits.apple', + }, + }, + ], + }, + + { + code: ` + const fruits = { apple: 'apple' }; + + export declare function processFruit< + F extends Record, + >(fruit: F): void; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 5, + column: 34, + endColumn: 47, + data: { + name: 'typeof fruits', + }, + }, + ], + }, ], }); From ff2c0a8692686d795968dd8f8ad8577de4aeca49 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Mon, 8 Jul 2024 08:36:10 +0300 Subject: [PATCH 66/66] simplify tsconfig --- packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json b/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json index 87cfb2c4b422..6168cfcb8d54 100644 --- a/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json +++ b/packages/eslint-plugin/tests/fixtures/tsconfig-with-dom.json @@ -1,9 +1,6 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "target": "es5", - "module": "ESNext", - "strict": true, - "esModuleInterop": true, "lib": ["esnext", "DOM"] } }