From eb02b4d84d8997254955930ff0042699cdac6f9d Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Wed, 14 Dec 2022 01:21:34 +0100 Subject: [PATCH 01/40] =?UTF-8?q?=F0=9F=9A=A7=20key-spacing=20for=20interf?= =?UTF-8?q?ace=20on=20default=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 90 +++++++++++++++++++ .../src/util/getESLintCoreRule.ts | 1 + .../tests/rules/key-spacing.test.ts | 28 ++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 25 ++++++ 4 files changed, 144 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/key-spacing.ts create mode 100644 packages/eslint-plugin/tests/rules/key-spacing.test.ts diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts new file mode 100644 index 00000000000..8d5c0806ac4 --- /dev/null +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -0,0 +1,90 @@ +/* eslint-disable no-console */ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import * as util from '../util'; +import { getESLintCoreRule } from '../util/getESLintCoreRule'; + +const baseRule = getESLintCoreRule('key-spacing'); + +export type Options = util.InferOptionsTypeFromRule; +export type MessageIds = util.InferMessageIdsTypeFromRule; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const baseSchema = Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema; + +export default util.createRule({ + name: 'key-spacing', + meta: { + type: 'layout', + docs: { + description: + 'Enforce consistent spacing between keys and values in types and interfaces', + recommended: false, + extendsBaseRule: true, + }, + fixable: 'whitespace', + hasSuggestions: baseRule.meta.hasSuggestions, + schema: [baseSchema], + messages: baseRule.meta.messages, + }, + defaultOptions: [{}], + + create(context) { + const sourceCode = context.getSourceCode(); + const baseRules = baseRule.create(context); + return { + ...baseRules, + "TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"( + node: TSESTree.TSTypeAliasDeclaration, + ): void { + console.log('...'); + // Todo + }, + TSInterfaceDeclaration(node): void { + const interfaceBody = node.body; + + let minStart = 0; + + for (const node of interfaceBody.body) { + if ( + node.type === AST_NODE_TYPES.TSPropertySignature && + node.typeAnnotation + ) { + // In case of single-line interface declaration, skip rule + if (node.loc.start.line === interfaceBody.loc.start.line) { + return; + } + + minStart = Math.max( + minStart, + node.typeAnnotation.loc.start.column + ': '.length, + ); + } + } + + for (const node of interfaceBody.body) { + if ( + node.type === AST_NODE_TYPES.TSPropertySignature && + node.typeAnnotation + ) { + const start = node.typeAnnotation.typeAnnotation.loc.start.column; + + if (start !== minStart) { + context.report({ + node, + messageId: start > minStart ? 'extraValue' : 'missingValue', + data: { + computed: '', + key: sourceCode.getText(node.key), + }, + }); + } + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index 1678903acd3..80962a677b0 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -12,6 +12,7 @@ interface RuleMap { 'dot-notation': typeof import('eslint/lib/rules/dot-notation'); indent: typeof import('eslint/lib/rules/indent'); 'init-declarations': typeof import('eslint/lib/rules/init-declarations'); + 'key-spacing': typeof import('eslint/lib/rules/key-spacing'); 'keyword-spacing': typeof import('eslint/lib/rules/keyword-spacing'); 'lines-between-class-members': typeof import('eslint/lib/rules/lines-between-class-members'); 'no-dupe-args': typeof import('eslint/lib/rules/no-dupe-args'); diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts new file mode 100644 index 00000000000..66a632a7843 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -0,0 +1,28 @@ +/* eslint-disable eslint-comments/no-use */ +// this rule tests the new lines, which prettier will want to fix and break the tests +/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */ +/* eslint-enable eslint-comments/no-use */ +import rule from '../../src/rules/key-spacing'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('key-spacing', rule, { + valid: [ + { + code: 'interface X {\n a: number;\n abc: string\n};', + }, + ], + invalid: [ + { + code: 'interface X {\n a: number;\n abc: string\n};', + errors: [{ messageId: 'missingValue' }], + }, + { + code: 'interface X {\n a: number;\n abc: string\n};', + errors: [{ messageId: 'extraValue' }], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 09b54ae4a51..82825487f86 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -141,6 +141,31 @@ declare module 'eslint/lib/rules/indent' { export = rule; } +declare module 'eslint/lib/rules/key-spacing' { + import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import type { RuleFunction } from '@typescript-eslint/utils/dist/ts-eslint'; + + type Options = [ + { + beforeColon?: boolean; + afterColon?: boolean; + mode?: 'strict' | 'minimum'; + align?: 'value' | 'colon'; + }, + ]; + type MessageIds = 'extraKey' | 'extraValue' | 'missingKey' | 'missingValue'; + + const rule: TSESLint.RuleModule< + MessageIds, + Options, + { + ObjectExpression: RuleFunction; + Property: RuleFunction; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/keyword-spacing' { import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type { RuleFunction } from '@typescript-eslint/utils/dist/ts-eslint'; From 6d78fe8798b5fe56cf5321e0636cf2148d4c0ec1 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Wed, 14 Dec 2022 01:51:58 +0100 Subject: [PATCH 02/40] =?UTF-8?q?=F0=9F=9A=A7=20Support=20type=20literals?= =?UTF-8?q?=20as=20welll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 85 +++++++++---------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 8d5c0806ac4..1180426fc7a 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -35,56 +34,56 @@ export default util.createRule({ create(context) { const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); - return { - ...baseRules, - "TSTypeAliasDeclaration[typeAnnotation.type='TSTypeLiteral']"( - node: TSESTree.TSTypeAliasDeclaration, - ): void { - console.log('...'); - // Todo - }, - TSInterfaceDeclaration(node): void { - const interfaceBody = node.body; - let minStart = 0; + function validateBody( + body: TSESTree.TSTypeLiteral | TSESTree.TSInterfaceBody, + ): void { + let minStart = 0; - for (const node of interfaceBody.body) { - if ( - node.type === AST_NODE_TYPES.TSPropertySignature && - node.typeAnnotation - ) { - // In case of single-line interface declaration, skip rule - if (node.loc.start.line === interfaceBody.loc.start.line) { - return; - } + // In case of single-line interface declaration, skip rule + if (body.loc.start.line === body.loc.end.line) { + return; + } - minStart = Math.max( - minStart, - node.typeAnnotation.loc.start.column + ': '.length, - ); - } + const members = 'members' in body ? body.members : body.body; + + for (const node of members) { + if ( + (node.type === AST_NODE_TYPES.TSPropertySignature || + node.type === AST_NODE_TYPES.TSIndexSignature) && + node.typeAnnotation + ) { + minStart = Math.max( + minStart, + node.typeAnnotation.loc.start.column + ': '.length, + ); } + } - for (const node of interfaceBody.body) { - if ( - node.type === AST_NODE_TYPES.TSPropertySignature && - node.typeAnnotation - ) { - const start = node.typeAnnotation.typeAnnotation.loc.start.column; + for (const node of members) { + if ( + node.type === AST_NODE_TYPES.TSPropertySignature && + node.typeAnnotation + ) { + const start = node.typeAnnotation.typeAnnotation.loc.start.column; - if (start !== minStart) { - context.report({ - node, - messageId: start > minStart ? 'extraValue' : 'missingValue', - data: { - computed: '', - key: sourceCode.getText(node.key), - }, - }); - } + if (start !== minStart) { + context.report({ + node, + messageId: start > minStart ? 'extraValue' : 'missingValue', + data: { + computed: '', + key: sourceCode.getText(node.key), + }, + }); } } - }, + } + } + return { + ...baseRules, + TSTypeLiteral: validateBody, + TSInterfaceBody: validateBody, }; }, }); From 259a3ca7ae4daac6ed0c6bf1a875f09392706c82 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 00:20:17 +0100 Subject: [PATCH 03/40] =?UTF-8?q?=F0=9F=9A=A7=20Add=20full=20typing=20for?= =?UTF-8?q?=20the=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/key-spacing.ts | 198 ++++++++++++++++-- .../tests/rules/key-spacing.test.ts | 22 ++ .../eslint-plugin/typings/eslint-rules.d.ts | 12 +- 4 files changed, 215 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 8a3c2bbf437..f7e51fdabd5 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -22,6 +22,7 @@ import explicitModuleBoundaryTypes from './explicit-module-boundary-types'; import funcCallSpacing from './func-call-spacing'; import indent from './indent'; import initDeclarations from './init-declarations'; +import keySpacing from './key-spacing'; import keywordSpacing from './keyword-spacing'; import linesBetweenClassMembers from './lines-between-class-members'; import memberDelimiterStyle from './member-delimiter-style'; @@ -153,6 +154,7 @@ export default { 'func-call-spacing': funcCallSpacing, indent: indent, 'init-declarations': initDeclarations, + 'key-spacing': keySpacing, 'keyword-spacing': keywordSpacing, 'lines-between-class-members': linesBetweenClassMembers, 'member-delimiter-style': memberDelimiterStyle, diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 1180426fc7a..5164d0d8b3b 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -31,53 +31,215 @@ export default util.createRule({ }, defaultOptions: [{}], - create(context) { + create(context, [options]) { const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); - function validateBody( - body: TSESTree.TSTypeLiteral | TSESTree.TSInterfaceBody, + function checkBeforeColon( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + key: TSESTree.PropertyName | TSESTree.Parameter, + nBeforeColon: number, ): void { - let minStart = 0; + const colon = node.typeAnnotation!.loc.start.column; + const keyEnd = key.loc.end.column; + const expectedDiff = nBeforeColon; + if (colon - keyEnd !== expectedDiff) { + context.report({ + node, + messageId: colon - keyEnd > expectedDiff ? 'extraKey' : 'missingKey', + data: { + computed: '', + key: sourceCode.getText(key), + }, + }); + } + } - // In case of single-line interface declaration, skip rule - if (body.loc.start.line === body.loc.end.line) { - return; + function checkAfterColon( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + key: TSESTree.PropertyName | TSESTree.Parameter, + nAfterColon: number, + ): void { + const colon = node.typeAnnotation!.loc.start.column; + const typeStart = node.typeAnnotation!.typeAnnotation.loc.start.column; + const expectedDiff = nAfterColon + 1; + if (typeStart - colon !== expectedDiff) { + context.report({ + node, + messageId: + typeStart - colon > expectedDiff ? 'extraValue' : 'missingValue', + data: { + computed: '', + key: sourceCode.getText(key), + }, + }); } + } - const members = 'members' in body ? body.members : body.body; + function checkAlignGroup(group: TSESTree.TypeElement[]): void { + let alignColumn = 0; + const align = + (typeof options.align === 'object' + ? options.align.on + : options.align) ?? 'colon'; + const beforeColon = + (typeof options.align === 'object' + ? options.align.beforeColon + : options.multiLine + ? options.multiLine.beforeColon + : options.beforeColon) ?? false; + const nBeforeColon = beforeColon ? 1 : 0; + const afterColon = + (typeof options.align === 'object' + ? options.align.afterColon + : options.multiLine + ? options.multiLine.afterColon + : options.beforeColon) ?? true; + const nAfterColon = afterColon ? 1 : 0; - for (const node of members) { + for (const node of group) { if ( (node.type === AST_NODE_TYPES.TSPropertySignature || node.type === AST_NODE_TYPES.TSIndexSignature) && node.typeAnnotation ) { - minStart = Math.max( - minStart, - node.typeAnnotation.loc.start.column + ': '.length, + const key = + 'key' in node + ? node.key + : node.parameters[node.parameters.length - 1]; + alignColumn = Math.max( + alignColumn, + align === 'colon' + ? key.loc.end.column + nBeforeColon + : node.typeAnnotation.loc.start.column + + ':'.length + + nAfterColon + + nBeforeColon, ); } } - for (const node of members) { + for (const node of group) { if ( - node.type === AST_NODE_TYPES.TSPropertySignature && + (node.type === AST_NODE_TYPES.TSPropertySignature || + node.type === AST_NODE_TYPES.TSIndexSignature) && node.typeAnnotation ) { - const start = node.typeAnnotation.typeAnnotation.loc.start.column; + const key = + 'key' in node + ? node.key + : node.parameters[node.parameters.length - 1]; + const start = + align === 'colon' + ? node.typeAnnotation.loc.start.column + : node.typeAnnotation.typeAnnotation.loc.start.column; - if (start !== minStart) { + if (start !== alignColumn) { context.report({ node, - messageId: start > minStart ? 'extraValue' : 'missingValue', + messageId: start > alignColumn ? 'extraValue' : 'missingValue', data: { computed: '', - key: sourceCode.getText(node.key), + key: sourceCode.getText(key), }, }); } + + if (align === 'colon') { + checkAfterColon(node, key, nAfterColon); + } else { + checkBeforeColon(node, key, nBeforeColon); + } + } + } + } + + function checkIndividualNode( + node: TSESTree.TypeElement, + { singleLine }: { singleLine: boolean }, + ): void { + const beforeColon = + (singleLine + ? options.singleLine + ? options.singleLine.beforeColon + : options.beforeColon + : options.multiLine + ? options.multiLine.beforeColon + : options.beforeColon) ?? false; + const nBeforeColon = beforeColon ? 1 : 0; + const afterColon = + (singleLine + ? options.singleLine + ? options.singleLine.afterColon + : options.afterColon + : options.multiLine + ? options.multiLine.afterColon + : options.afterColon) ?? true; + const nAfterColon = afterColon ? 1 : 0; + + if ( + (node.type === AST_NODE_TYPES.TSPropertySignature || + node.type === AST_NODE_TYPES.TSIndexSignature) && + node.typeAnnotation + ) { + const key = + 'key' in node + ? node.key + : node.parameters[node.parameters.length - 1]; + + checkBeforeColon(node, key, nBeforeColon); + checkAfterColon(node, key, nAfterColon); + } + } + + function validateBody( + body: TSESTree.TSTypeLiteral | TSESTree.TSInterfaceBody, + ): void { + const isSingleLine = body.loc.start.line === body.loc.end.line; + + const members = 'members' in body ? body.members : body.body; + + let alignGroups: TSESTree.TypeElement[][] = []; + let unalignedElements: TSESTree.TypeElement[] = []; + + if (options.align) { + let currentAlignGroup: TSESTree.TypeElement[] = []; + alignGroups.push(currentAlignGroup); + + for (const node of members) { + const prevNode = currentAlignGroup.length + ? currentAlignGroup[currentAlignGroup.length - 1] + : null; + + if (prevNode?.loc.start.line === node.loc.start.line - 1) { + currentAlignGroup.push(node); + } else if (prevNode?.loc.start.line === node.loc.start.line) { + if (currentAlignGroup.length) { + unalignedElements.push(currentAlignGroup.pop()!); + } + unalignedElements.push(node); + } else { + currentAlignGroup = [node]; + alignGroups.push(currentAlignGroup); + } } + + unalignedElements.push( + ...alignGroups + .filter(group => group.length === 1) + .flatMap(group => group), + ); + alignGroups = alignGroups.filter(group => group.length >= 2); + } else { + unalignedElements = members; + } + + for (const group of alignGroups) { + checkAlignGroup(group); + } + + for (const node of unalignedElements) { + checkIndividualNode(node, { singleLine: isSingleLine }); } } return { diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 66a632a7843..75388bf76c4 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -13,16 +13,38 @@ ruleTester.run('key-spacing', rule, { valid: [ { code: 'interface X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + }, + { + // A blank line between two keys resets the alignment + code: 'interface X {\n a: number;\n\n abc: string\n};', + options: [{ align: 'value' }], }, ], invalid: [ { code: 'interface X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { code: 'interface X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }], + }, + { + // A blank line between two keys resets the alignment + code: 'interface X {\n a: number;\n\n abc : string\n};', + options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, + { + code: 'interface X {\n [x: number]: string;\n}', + errors: [{ messageId: 'extraValue' }], + }, + { + code: 'interface X {\n [x: number]:string;\n}', + errors: [{ messageId: 'missingValue' }], + }, ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 82825487f86..434da4cbd79 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -150,7 +150,17 @@ declare module 'eslint/lib/rules/key-spacing' { beforeColon?: boolean; afterColon?: boolean; mode?: 'strict' | 'minimum'; - align?: 'value' | 'colon'; + align?: + | 'value' + | 'colon' + | { + on: 'value' | 'colon'; + beforeColon?: boolean; + afterColon?: boolean; + mode?: 'strict' | 'minimum'; + }; + singleLine?: { beforeColon?: boolean; afterColon?: boolean }; + multiLine?: { beforeColon?: boolean; afterColon?: boolean }; }, ]; type MessageIds = 'extraKey' | 'extraValue' | 'missingKey' | 'missingValue'; From 8f32b5d357b52fd29580cf0d7e23e049caa78268 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 00:46:35 +0100 Subject: [PATCH 04/40] =?UTF-8?q?=F0=9F=9A=A7=20Add=20'mode'=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 40 +++++++++++++++---- .../eslint-plugin/typings/eslint-rules.d.ts | 13 +++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 5164d0d8b3b..9671a518d95 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -39,11 +39,16 @@ export default util.createRule({ node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, key: TSESTree.PropertyName | TSESTree.Parameter, nBeforeColon: number, + mode: 'strict' | 'minimum', ): void { const colon = node.typeAnnotation!.loc.start.column; const keyEnd = key.loc.end.column; const expectedDiff = nBeforeColon; - if (colon - keyEnd !== expectedDiff) { + if ( + mode === 'strict' + ? colon - keyEnd !== expectedDiff + : colon - keyEnd < expectedDiff + ) { context.report({ node, messageId: colon - keyEnd > expectedDiff ? 'extraKey' : 'missingKey', @@ -59,11 +64,16 @@ export default util.createRule({ node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, key: TSESTree.PropertyName | TSESTree.Parameter, nAfterColon: number, + mode: 'strict' | 'minimum', ): void { const colon = node.typeAnnotation!.loc.start.column; const typeStart = node.typeAnnotation!.typeAnnotation.loc.start.column; const expectedDiff = nAfterColon + 1; - if (typeStart - colon !== expectedDiff) { + if ( + mode === 'strict' + ? typeStart - colon !== expectedDiff + : typeStart - colon < expectedDiff + ) { context.report({ node, messageId: @@ -81,7 +91,7 @@ export default util.createRule({ const align = (typeof options.align === 'object' ? options.align.on - : options.align) ?? 'colon'; + : options.multiLine?.align ?? options.align) ?? 'colon'; const beforeColon = (typeof options.align === 'object' ? options.align.beforeColon @@ -96,6 +106,12 @@ export default util.createRule({ ? options.multiLine.afterColon : options.beforeColon) ?? true; const nAfterColon = afterColon ? 1 : 0; + const mode = + (typeof options.align === 'object' + ? options.align.mode + : options.multiLine + ? options.multiLine.mode + : options.mode) ?? 'strict'; for (const node of group) { if ( @@ -146,9 +162,9 @@ export default util.createRule({ } if (align === 'colon') { - checkAfterColon(node, key, nAfterColon); + checkAfterColon(node, key, nAfterColon, mode); } else { - checkBeforeColon(node, key, nBeforeColon); + checkBeforeColon(node, key, nBeforeColon, mode); } } } @@ -176,6 +192,14 @@ export default util.createRule({ ? options.multiLine.afterColon : options.afterColon) ?? true; const nAfterColon = afterColon ? 1 : 0; + const mode = + (singleLine + ? options.singleLine + ? options.singleLine.mode + : options.mode + : options.multiLine + ? options.multiLine.mode + : options.mode) ?? 'strict'; if ( (node.type === AST_NODE_TYPES.TSPropertySignature || @@ -187,8 +211,8 @@ export default util.createRule({ ? node.key : node.parameters[node.parameters.length - 1]; - checkBeforeColon(node, key, nBeforeColon); - checkAfterColon(node, key, nAfterColon); + checkBeforeColon(node, key, nBeforeColon, mode); + checkAfterColon(node, key, nAfterColon, mode); } } @@ -202,7 +226,7 @@ export default util.createRule({ let alignGroups: TSESTree.TypeElement[][] = []; let unalignedElements: TSESTree.TypeElement[] = []; - if (options.align) { + if (options.align || options.multiLine?.align) { let currentAlignGroup: TSESTree.TypeElement[] = []; alignGroups.push(currentAlignGroup); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 434da4cbd79..4315edb2354 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -159,8 +159,17 @@ declare module 'eslint/lib/rules/key-spacing' { afterColon?: boolean; mode?: 'strict' | 'minimum'; }; - singleLine?: { beforeColon?: boolean; afterColon?: boolean }; - multiLine?: { beforeColon?: boolean; afterColon?: boolean }; + singleLine?: { + beforeColon?: boolean; + afterColon?: boolean; + mode?: 'strict' | 'minimum'; + }; + multiLine?: { + beforeColon?: boolean; + afterColon?: boolean; + mode?: 'strict' | 'minimum'; + align?: 'value' | 'colon'; + }; }, ]; type MessageIds = 'extraKey' | 'extraValue' | 'missingKey' | 'missingValue'; From 34bb6d8bb87300f42a8104c761aca34a75a08b66 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 02:06:46 +0100 Subject: [PATCH 05/40] =?UTF-8?q?=F0=9F=90=9B=20Fix=20index=20signatures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 107 ++++++++++++------ .../tests/rules/key-spacing.test.ts | 3 +- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 9671a518d95..f6a3682a737 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -35,26 +35,77 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); + /** + * To handle index signatures, to get the whole text for the parameters + */ + function getKeyText( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + ): string { + if ('key' in node) { + return sourceCode.getText(node); + } + + const code = sourceCode.getText(node); + const lastParam = node.parameters[node.parameters.length - 1]; + return code.slice( + 0, + code.indexOf(']', lastParam.range[1] - node.range[0]) + 1, + ); + } + + /** + * To handle index signatures, be able to get the end position of the parameters + */ + function getKeyLocEnd( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + ): TSESTree.Position { + if ('key' in node) { + return node.key.loc.end; + } + + // For index signatures, there's no easy way to get the location of the ending ']', we need to look at the source code + const code = sourceCode.getText(node); + const lastParam = node.parameters[node.parameters.length - 1]; + + const remaining = code.slice( + lastParam.range[1] - node.range[0], + code.indexOf(']', lastParam.range[1] - node.range[0]) + 1, + ); + const lines = remaining.split('\n'); + + if (lines.length === 1) { + return { + line: lastParam.loc.end.line, + column: lastParam.loc.end.column + remaining.length, + }; + } + + return { + line: lastParam.loc.end.line + lines.length - 1, + column: lines[lines.length - 1].length, + }; + } + function checkBeforeColon( node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, - key: TSESTree.PropertyName | TSESTree.Parameter, nBeforeColon: number, mode: 'strict' | 'minimum', ): void { const colon = node.typeAnnotation!.loc.start.column; - const keyEnd = key.loc.end.column; + const keyEnd = getKeyLocEnd(node); const expectedDiff = nBeforeColon; if ( mode === 'strict' - ? colon - keyEnd !== expectedDiff - : colon - keyEnd < expectedDiff + ? colon - keyEnd.column !== expectedDiff + : colon - keyEnd.column < expectedDiff ) { context.report({ node, - messageId: colon - keyEnd > expectedDiff ? 'extraKey' : 'missingKey', + messageId: + colon - keyEnd.column > expectedDiff ? 'extraKey' : 'missingKey', data: { computed: '', - key: sourceCode.getText(key), + key: getKeyText(node), }, }); } @@ -62,7 +113,6 @@ export default util.createRule({ function checkAfterColon( node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, - key: TSESTree.PropertyName | TSESTree.Parameter, nAfterColon: number, mode: 'strict' | 'minimum', ): void { @@ -80,13 +130,13 @@ export default util.createRule({ typeStart - colon > expectedDiff ? 'extraValue' : 'missingValue', data: { computed: '', - key: sourceCode.getText(key), + key: getKeyText(node), }, }); } } - function checkAlignGroup(group: TSESTree.TypeElement[]): void { + function checkAlignGroup(group: TSESTree.Node[]): void { let alignColumn = 0; const align = (typeof options.align === 'object' @@ -119,14 +169,10 @@ export default util.createRule({ node.type === AST_NODE_TYPES.TSIndexSignature) && node.typeAnnotation ) { - const key = - 'key' in node - ? node.key - : node.parameters[node.parameters.length - 1]; alignColumn = Math.max( alignColumn, align === 'colon' - ? key.loc.end.column + nBeforeColon + ? getKeyLocEnd(node).column + nBeforeColon : node.typeAnnotation.loc.start.column + ':'.length + nAfterColon + @@ -141,10 +187,6 @@ export default util.createRule({ node.type === AST_NODE_TYPES.TSIndexSignature) && node.typeAnnotation ) { - const key = - 'key' in node - ? node.key - : node.parameters[node.parameters.length - 1]; const start = align === 'colon' ? node.typeAnnotation.loc.start.column @@ -156,22 +198,22 @@ export default util.createRule({ messageId: start > alignColumn ? 'extraValue' : 'missingValue', data: { computed: '', - key: sourceCode.getText(key), + key: getKeyText(node), }, }); } if (align === 'colon') { - checkAfterColon(node, key, nAfterColon, mode); + checkAfterColon(node, nAfterColon, mode); } else { - checkBeforeColon(node, key, nBeforeColon, mode); + checkBeforeColon(node, nBeforeColon, mode); } } } } function checkIndividualNode( - node: TSESTree.TypeElement, + node: TSESTree.Node, { singleLine }: { singleLine: boolean }, ): void { const beforeColon = @@ -206,28 +248,26 @@ export default util.createRule({ node.type === AST_NODE_TYPES.TSIndexSignature) && node.typeAnnotation ) { - const key = - 'key' in node - ? node.key - : node.parameters[node.parameters.length - 1]; - - checkBeforeColon(node, key, nBeforeColon, mode); - checkAfterColon(node, key, nAfterColon, mode); + checkBeforeColon(node, nBeforeColon, mode); + checkAfterColon(node, nAfterColon, mode); } } function validateBody( - body: TSESTree.TSTypeLiteral | TSESTree.TSInterfaceBody, + body: + | TSESTree.TSTypeLiteral + | TSESTree.TSInterfaceBody + | TSESTree.ClassBody, ): void { const isSingleLine = body.loc.start.line === body.loc.end.line; const members = 'members' in body ? body.members : body.body; - let alignGroups: TSESTree.TypeElement[][] = []; - let unalignedElements: TSESTree.TypeElement[] = []; + let alignGroups: TSESTree.Node[][] = []; + let unalignedElements: TSESTree.Node[] = []; if (options.align || options.multiLine?.align) { - let currentAlignGroup: TSESTree.TypeElement[] = []; + let currentAlignGroup: TSESTree.Node[] = []; alignGroups.push(currentAlignGroup); for (const node of members) { @@ -270,6 +310,7 @@ export default util.createRule({ ...baseRules, TSTypeLiteral: validateBody, TSInterfaceBody: validateBody, + ClassBody: validateBody, }; }, }); diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 75388bf76c4..6bcf77c4bb8 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -33,10 +33,9 @@ ruleTester.run('key-spacing', rule, { errors: [{ messageId: 'extraValue' }], }, { - // A blank line between two keys resets the alignment code: 'interface X {\n a: number;\n\n abc : string\n};', options: [{ align: 'value' }], - errors: [{ messageId: 'extraValue' }], + errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, { code: 'interface X {\n [x: number]: string;\n}', From ecaafa7926df9c4b6ab8a78cef9b3e808cd1df8b Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 02:26:45 +0100 Subject: [PATCH 06/40] =?UTF-8?q?=E2=9C=A8=20Support=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 28 ++++++++++++++----- .../tests/rules/key-spacing.test.ts | 24 +++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index f6a3682a737..438332930af 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -39,10 +39,13 @@ export default util.createRule({ * To handle index signatures, to get the whole text for the parameters */ function getKeyText( - node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + node: + | TSESTree.TSIndexSignature + | TSESTree.TSPropertySignature + | TSESTree.PropertyDefinition, ): string { if ('key' in node) { - return sourceCode.getText(node); + return sourceCode.getText(node.key); } const code = sourceCode.getText(node); @@ -57,7 +60,10 @@ export default util.createRule({ * To handle index signatures, be able to get the end position of the parameters */ function getKeyLocEnd( - node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + node: + | TSESTree.TSIndexSignature + | TSESTree.TSPropertySignature + | TSESTree.PropertyDefinition, ): TSESTree.Position { if ('key' in node) { return node.key.loc.end; @@ -87,7 +93,10 @@ export default util.createRule({ } function checkBeforeColon( - node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + node: + | TSESTree.TSIndexSignature + | TSESTree.TSPropertySignature + | TSESTree.PropertyDefinition, nBeforeColon: number, mode: 'strict' | 'minimum', ): void { @@ -112,7 +121,10 @@ export default util.createRule({ } function checkAfterColon( - node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + node: + | TSESTree.TSIndexSignature + | TSESTree.TSPropertySignature + | TSESTree.PropertyDefinition, nAfterColon: number, mode: 'strict' | 'minimum', ): void { @@ -166,7 +178,8 @@ export default util.createRule({ for (const node of group) { if ( (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature) && + node.type === AST_NODE_TYPES.TSIndexSignature || + node.type === AST_NODE_TYPES.PropertyDefinition) && node.typeAnnotation ) { alignColumn = Math.max( @@ -184,7 +197,8 @@ export default util.createRule({ for (const node of group) { if ( (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature) && + node.type === AST_NODE_TYPES.TSIndexSignature || + node.type === AST_NODE_TYPES.PropertyDefinition) && node.typeAnnotation ) { const start = diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 6bcf77c4bb8..f43974f91f9 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -16,10 +16,17 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], }, { - // A blank line between two keys resets the alignment code: 'interface X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'class X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + }, + { + code: 'class X {\n a: number;\n\n abc: string\n};', + options: [{ align: 'value' }], + }, ], invalid: [ { @@ -27,16 +34,31 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, + { + code: 'class X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, + { + code: 'class X {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }], + }, { code: 'interface X {\n a: number;\n\n abc : string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, + { + code: 'class X {\n a: number;\n\n abc : string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], + }, { code: 'interface X {\n [x: number]: string;\n}', errors: [{ messageId: 'extraValue' }], From b17665be6f99caae4a99a80fd8fb511befcbbce3 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 02:46:05 +0100 Subject: [PATCH 07/40] =?UTF-8?q?=F0=9F=A9=B9=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 14 +++++-- .../tests/rules/key-spacing.test.ts | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 438332930af..2bb96960c64 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -166,7 +166,7 @@ export default util.createRule({ ? options.align.afterColon : options.multiLine ? options.multiLine.afterColon - : options.beforeColon) ?? true; + : options.afterColon) ?? true; const nAfterColon = afterColon ? 1 : 0; const mode = (typeof options.align === 'object' @@ -209,7 +209,14 @@ export default util.createRule({ if (start !== alignColumn) { context.report({ node, - messageId: start > alignColumn ? 'extraValue' : 'missingValue', + messageId: + start > alignColumn + ? align === 'colon' + ? 'extraKey' + : 'extraValue' + : align === 'colon' + ? 'missingKey' + : 'missingValue', data: { computed: '', key: getKeyText(node), @@ -259,7 +266,8 @@ export default util.createRule({ if ( (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature) && + node.type === AST_NODE_TYPES.TSIndexSignature || + node.type === AST_NODE_TYPES.PropertyDefinition) && node.typeAnnotation ) { checkBeforeColon(node, nBeforeColon, mode); diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index f43974f91f9..4bdfec3f522 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -11,6 +11,7 @@ const ruleTester = new RuleTester({ ruleTester.run('key-spacing', rule, { valid: [ + // align: value { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], @@ -27,8 +28,35 @@ ruleTester.run('key-spacing', rule, { code: 'class X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'type X = {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + }, + { + code: 'type X = {\n a: number;\n\n abc: string\n};', + options: [{ align: 'value' }], + }, + // align: colon + { + code: 'interface X {\n a : number;\n abc: string\n};', + options: [{ align: 'colon' }], + }, + { + code: 'interface X {\n a :number;\n abc:string\n};', + options: [{ align: 'colon', afterColon: false }], + }, + // no align + { + code: 'interface X {\n a: number;\n abc: string\n};', + options: [{}], + }, + { + code: 'interface X {\n a : number;\n abc : string\n};', + options: [{ beforeColon: true }], + }, ], invalid: [ + // align: value { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], @@ -39,6 +67,11 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, + { + code: 'type X = {\n a: number;\n abc: string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], @@ -59,6 +92,13 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, + // align: colon + { + code: 'interface X {\n a : number;\n abc: string\n};', + options: [{ align: 'colon' }], + errors: [{ messageId: 'extraKey' }], + }, + // no align { code: 'interface X {\n [x: number]: string;\n}', errors: [{ messageId: 'extraValue' }], From 18bb8ab54833c6f39d57fc53e6189709aaf2f0f0 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 02:54:40 +0100 Subject: [PATCH 08/40] =?UTF-8?q?=E2=9C=85=20Add=20tests=20on=20mode,=20mu?= =?UTF-8?q?ltiLine,=20singleLine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 4bdfec3f522..1a8f6dc187a 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -36,6 +36,10 @@ ruleTester.run('key-spacing', rule, { code: 'type X = {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'type X = {\n a : number;\n abc: string\n};', + options: [{ align: 'value', mode: 'minimum' }], + }, // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', @@ -45,6 +49,10 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a :number;\n abc:string\n};', options: [{ align: 'colon', afterColon: false }], }, + { + code: 'interface X {\n a : number;\n abc: string\n};', + options: [{ align: 'colon', mode: 'minimum' }], + }, // no align { code: 'interface X {\n a: number;\n abc: string\n};', @@ -54,6 +62,25 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a : number;\n abc : string\n};', options: [{ beforeColon: true }], }, + // singleLine / multiLine + { + code: 'interface X {\n a : number;\n abc : string\n};', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + }, + { + code: 'interface X { a:number; abc:string; };', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + }, ], invalid: [ // align: value @@ -67,6 +94,11 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, + { + code: 'class X {\n a: number;\n abc: string\n};', + options: [{ align: 'value', mode: 'minimum' }], + errors: [{ messageId: 'missingValue' }], + }, { code: 'type X = {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], @@ -107,5 +139,46 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n [x: number]:string;\n}', errors: [{ messageId: 'missingValue' }], }, + // singleLine / multiLine + { + code: 'interface X {\n a:number;\n abc:string\n};', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + errors: [ + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + ], + }, + { + code: 'interface X { a : number; abc : string; };', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + errors: [ + { messageId: 'extraKey' }, + { messageId: 'extraValue' }, + { messageId: 'extraKey' }, + { messageId: 'extraValue' }, + ], + }, + { + code: 'interface X { a : number; abc : string; };', + options: [ + { + singleLine: { beforeColon: false, afterColon: true }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + errors: [{ messageId: 'extraKey' }, { messageId: 'extraKey' }], + }, ], }); From 4113478b8191b635683d8a519d288b043a499de1 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 03:15:09 +0100 Subject: [PATCH 09/40] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Allow=20options.m?= =?UTF-8?q?ultiline.align=20to=20be=20an=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/key-spacing.ts | 13 ++++++++++--- packages/eslint-plugin/typings/eslint-rules.d.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 2bb96960c64..911c39a0d2e 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -158,21 +158,28 @@ export default util.createRule({ (typeof options.align === 'object' ? options.align.beforeColon : options.multiLine - ? options.multiLine.beforeColon + ? typeof options.multiLine.align === 'object' + ? options.multiLine.align.beforeColon + : options.multiLine.beforeColon : options.beforeColon) ?? false; const nBeforeColon = beforeColon ? 1 : 0; const afterColon = (typeof options.align === 'object' ? options.align.afterColon : options.multiLine - ? options.multiLine.afterColon + ? typeof options.multiLine.align === 'object' + ? options.multiLine.align.afterColon + : options.multiLine.afterColon : options.afterColon) ?? true; const nAfterColon = afterColon ? 1 : 0; const mode = (typeof options.align === 'object' ? options.align.mode : options.multiLine - ? options.multiLine.mode + ? typeof options.multiLine.align === 'object' + ? // same behavior as in original rule + options.multiLine.align.mode ?? options.multiLine.mode + : options.multiLine.mode : options.mode) ?? 'strict'; for (const node of group) { diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 4315edb2354..7eda886c91b 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -168,7 +168,15 @@ declare module 'eslint/lib/rules/key-spacing' { beforeColon?: boolean; afterColon?: boolean; mode?: 'strict' | 'minimum'; - align?: 'value' | 'colon'; + align?: + | 'value' + | 'colon' + | { + on: 'value' | 'colon'; + beforeColon?: boolean; + afterColon?: boolean; + mode?: 'strict' | 'minimum'; + }; }; }, ]; From 5e951ba1976e09f2b8329ede840c5233164dd249 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 09:49:04 +0100 Subject: [PATCH 10/40] =?UTF-8?q?=F0=9F=8E=A8=20Use=20ast=20utils=20to=20l?= =?UTF-8?q?ocate=20last=20character=20before=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 911c39a0d2e..11ca88f8189 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -35,6 +35,16 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); + /** + * Starting from the given a node (a property.key node here) looks forward + * until it finds the last token before a colon punctuator and returns it. + */ + function getLastTokenBeforeColon(node: TSESTree.Node): TSESTree.Token { + const colonToken = sourceCode.getTokenAfter(node, util.isColonToken)!; + + return sourceCode.getTokenBefore(colonToken)!; + } + /** * To handle index signatures, to get the whole text for the parameters */ @@ -52,7 +62,7 @@ export default util.createRule({ const lastParam = node.parameters[node.parameters.length - 1]; return code.slice( 0, - code.indexOf(']', lastParam.range[1] - node.range[0]) + 1, + getLastTokenBeforeColon(lastParam).range[1] - node.range[0], ); } @@ -69,27 +79,9 @@ export default util.createRule({ return node.key.loc.end; } - // For index signatures, there's no easy way to get the location of the ending ']', we need to look at the source code - const code = sourceCode.getText(node); - const lastParam = node.parameters[node.parameters.length - 1]; - - const remaining = code.slice( - lastParam.range[1] - node.range[0], - code.indexOf(']', lastParam.range[1] - node.range[0]) + 1, - ); - const lines = remaining.split('\n'); - - if (lines.length === 1) { - return { - line: lastParam.loc.end.line, - column: lastParam.loc.end.column + remaining.length, - }; - } - - return { - line: lastParam.loc.end.line + lines.length - 1, - column: lines[lines.length - 1].length, - }; + return getLastTokenBeforeColon( + node.parameters[node.parameters.length - 1], + ).loc.end; } function checkBeforeColon( From b12c530209f6a12f80ad1afdd8196a088f9b221d Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 10:17:31 +0100 Subject: [PATCH 11/40] =?UTF-8?q?=E2=9C=A8=20Support=20comments=20in-betwe?= =?UTF-8?q?en=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 113 ++++++++++++------ .../tests/rules/key-spacing.test.ts | 17 +++ 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 11ca88f8189..1f6027ed9a5 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -45,15 +45,30 @@ export default util.createRule({ return sourceCode.getTokenBefore(colonToken)!; } + /** + * Relevant nodes to our rule + * + * node.typeAnnotation will aways be defined, but no way to enforce that and keep + * the type compatible with TSEstree.Node (except maybe TS 4.9' "satisfies" keyword) + */ + type KeyTypeNode = + | TSESTree.TSIndexSignature + | TSESTree.TSPropertySignature + | TSESTree.PropertyDefinition; + + function isKeyTypeNode(node: TSESTree.Node): node is KeyTypeNode { + return ( + (node.type === AST_NODE_TYPES.TSPropertySignature || + node.type === AST_NODE_TYPES.TSIndexSignature || + node.type === AST_NODE_TYPES.PropertyDefinition) && + !!node.typeAnnotation + ); + } + /** * To handle index signatures, to get the whole text for the parameters */ - function getKeyText( - node: - | TSESTree.TSIndexSignature - | TSESTree.TSPropertySignature - | TSESTree.PropertyDefinition, - ): string { + function getKeyText(node: KeyTypeNode): string { if ('key' in node) { return sourceCode.getText(node.key); } @@ -85,10 +100,7 @@ export default util.createRule({ } function checkBeforeColon( - node: - | TSESTree.TSIndexSignature - | TSESTree.TSPropertySignature - | TSESTree.PropertyDefinition, + node: KeyTypeNode, nBeforeColon: number, mode: 'strict' | 'minimum', ): void { @@ -113,10 +125,7 @@ export default util.createRule({ } function checkAfterColon( - node: - | TSESTree.TSIndexSignature - | TSESTree.TSPropertySignature - | TSESTree.PropertyDefinition, + node: KeyTypeNode, nAfterColon: number, mode: 'strict' | 'minimum', ): void { @@ -140,6 +149,53 @@ export default util.createRule({ } } + // adapted from https://github.com/eslint/eslint/blob/ba74253e8bd63e9e163bbee0540031be77e39253/lib/rules/key-spacing.js#L356 + function continuesAlignGroup( + lastMember: TSESTree.Node, + candidate: TSESTree.Node, + ): boolean { + const groupEndLine = lastMember.loc.start.line; + const candidateValueStartLine = ( + isKeyTypeNode(candidate) ? candidate.typeAnnotation! : candidate + ).loc.start.line; + + if (candidateValueStartLine === groupEndLine) { + return false; + } + + if (candidateValueStartLine - groupEndLine === 1) { + return true; + } + + /* + * Check that the first comment is adjacent to the end of the group, the + * last comment is adjacent to the candidate property, and that successive + * comments are adjacent to each other. + */ + const leadingComments = sourceCode.getCommentsBefore(candidate); + + if ( + leadingComments.length && + leadingComments[0].loc.start.line - groupEndLine <= 1 && + candidateValueStartLine - + leadingComments[leadingComments.length - 1].loc.end.line <= + 1 + ) { + for (let i = 1; i < leadingComments.length; i++) { + if ( + leadingComments[i].loc.start.line - + leadingComments[i - 1].loc.end.line > + 1 + ) { + return false; + } + } + return true; + } + + return false; + } + function checkAlignGroup(group: TSESTree.Node[]): void { let alignColumn = 0; const align = @@ -175,17 +231,12 @@ export default util.createRule({ : options.mode) ?? 'strict'; for (const node of group) { - if ( - (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature || - node.type === AST_NODE_TYPES.PropertyDefinition) && - node.typeAnnotation - ) { + if (isKeyTypeNode(node)) { alignColumn = Math.max( alignColumn, align === 'colon' ? getKeyLocEnd(node).column + nBeforeColon - : node.typeAnnotation.loc.start.column + + : node.typeAnnotation!.loc.start.column + ':'.length + nAfterColon + nBeforeColon, @@ -194,16 +245,11 @@ export default util.createRule({ } for (const node of group) { - if ( - (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature || - node.type === AST_NODE_TYPES.PropertyDefinition) && - node.typeAnnotation - ) { + if (isKeyTypeNode(node)) { const start = align === 'colon' - ? node.typeAnnotation.loc.start.column - : node.typeAnnotation.typeAnnotation.loc.start.column; + ? node.typeAnnotation!.loc.start.column + : node.typeAnnotation!.typeAnnotation.loc.start.column; if (start !== alignColumn) { context.report({ @@ -263,12 +309,7 @@ export default util.createRule({ ? options.multiLine.mode : options.mode) ?? 'strict'; - if ( - (node.type === AST_NODE_TYPES.TSPropertySignature || - node.type === AST_NODE_TYPES.TSIndexSignature || - node.type === AST_NODE_TYPES.PropertyDefinition) && - node.typeAnnotation - ) { + if (isKeyTypeNode(node)) { checkBeforeColon(node, nBeforeColon, mode); checkAfterColon(node, nAfterColon, mode); } @@ -296,7 +337,7 @@ export default util.createRule({ ? currentAlignGroup[currentAlignGroup.length - 1] : null; - if (prevNode?.loc.start.line === node.loc.start.line - 1) { + if (prevNode && continuesAlignGroup(prevNode, node)) { currentAlignGroup.push(node); } else if (prevNode?.loc.start.line === node.loc.start.line) { if (currentAlignGroup.length) { diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 1a8f6dc187a..b5f115047d6 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -16,6 +16,18 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'interface X {\n a: number;\n // Some comment\n abc: string\n};', + options: [{ align: 'value' }], + }, + { + code: 'interface X {\n a: number;\n // Some comment\n // on multiple lines\n abc: string\n};', + options: [{ align: 'value' }], + }, + { + code: 'interface X {\n a: number;\n /**\n * Doc comment\n */\n abc: string\n};', + options: [{ align: 'value' }], + }, { code: 'interface X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], @@ -124,6 +136,11 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, + { + code: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }], + }, // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', From 729e6426531045ea7b8140d0ed38dc4349b7056b Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 10:26:51 +0100 Subject: [PATCH 12/40] =?UTF-8?q?=E2=9C=85=20Add=20test=20cases=20for=20ne?= =?UTF-8?q?sted=20type=20declarations=20&=20multiline=20type=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index b5f115047d6..8626dba07fc 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -52,6 +52,20 @@ ruleTester.run('key-spacing', rule, { code: 'type X = {\n a : number;\n abc: string\n};', options: [{ align: 'value', mode: 'minimum' }], }, + { + code: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} +`, + options: [{ align: 'value' }], + }, // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', @@ -141,6 +155,51 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, + { + code: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} +`, + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} +`, + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} +`, + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }], + }, // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', From d93c1a7cebcbdbd3624227cf34d03e5bbb958190 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 11:03:10 +0100 Subject: [PATCH 13/40] =?UTF-8?q?=E2=9C=A8=20Autofix=20for=20non-aligned?= =?UTF-8?q?=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 48 ++++++++++++------- .../tests/rules/key-spacing.test.ts | 19 ++++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 1f6027ed9a5..5b6aed6110a 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -106,16 +106,24 @@ export default util.createRule({ ): void { const colon = node.typeAnnotation!.loc.start.column; const keyEnd = getKeyLocEnd(node); - const expectedDiff = nBeforeColon; - if ( - mode === 'strict' - ? colon - keyEnd.column !== expectedDiff - : colon - keyEnd.column < expectedDiff - ) { + const difference = colon - keyEnd.column - nBeforeColon; + if (mode === 'strict' ? difference : difference < 0) { context.report({ node, - messageId: - colon - keyEnd.column > expectedDiff ? 'extraKey' : 'missingKey', + messageId: difference > 0 ? 'extraKey' : 'missingKey', + fix: fixer => { + if (difference > 0) { + return fixer.removeRange([ + node.typeAnnotation!.range[0] - difference, + node.typeAnnotation!.range[0], + ]); + } else { + return fixer.insertTextBefore( + node.typeAnnotation!, + ' '.repeat(-difference), + ); + } + }, data: { computed: '', key: getKeyText(node), @@ -131,16 +139,24 @@ export default util.createRule({ ): void { const colon = node.typeAnnotation!.loc.start.column; const typeStart = node.typeAnnotation!.typeAnnotation.loc.start.column; - const expectedDiff = nAfterColon + 1; - if ( - mode === 'strict' - ? typeStart - colon !== expectedDiff - : typeStart - colon < expectedDiff - ) { + const difference = typeStart - colon - 1 - nAfterColon; + if (mode === 'strict' ? difference : difference < 0) { context.report({ node, - messageId: - typeStart - colon > expectedDiff ? 'extraValue' : 'missingValue', + messageId: difference > 0 ? 'extraValue' : 'missingValue', + fix: fixer => { + if (difference > 0) { + return fixer.removeRange([ + node.typeAnnotation!.typeAnnotation.range[0] - difference, + node.typeAnnotation!.typeAnnotation.range[0], + ]); + } else { + return fixer.insertTextBefore( + node.typeAnnotation!.typeAnnotation, + ' '.repeat(-difference), + ); + } + }, data: { computed: '', key: getKeyText(node), diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 8626dba07fc..49de9d0ef4c 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -142,16 +142,19 @@ interface X { }, { code: 'interface X {\n a: number;\n\n abc : string\n};', + output: 'interface X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, { code: 'class X {\n a: number;\n\n abc : string\n};', + output: 'class X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, { code: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', + output: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, @@ -196,6 +199,17 @@ interface X { }, abc: string } +`, + output: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], @@ -209,15 +223,18 @@ interface X { // no align { code: 'interface X {\n [x: number]: string;\n}', + output: 'interface X {\n [x: number]: string;\n}', errors: [{ messageId: 'extraValue' }], }, { code: 'interface X {\n [x: number]:string;\n}', + output: 'interface X {\n [x: number]: string;\n}', errors: [{ messageId: 'missingValue' }], }, // singleLine / multiLine { code: 'interface X {\n a:number;\n abc:string\n};', + output: 'interface X {\n a : number;\n abc : string\n};', options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -233,6 +250,7 @@ interface X { }, { code: 'interface X { a : number; abc : string; };', + output: 'interface X { a:number; abc:string; };', options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -248,6 +266,7 @@ interface X { }, { code: 'interface X { a : number; abc : string; };', + output: 'interface X { a: number; abc: string; };', options: [ { singleLine: { beforeColon: false, afterColon: true }, From ee519e30c5d08ff59af584e43a291f691450bde2 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 11:22:24 +0100 Subject: [PATCH 14/40] =?UTF-8?q?=E2=9C=A8=20Autofix=20for=20aligned=20val?= =?UTF-8?q?ues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 72 +++++++++++-------- .../tests/rules/key-spacing.test.ts | 29 ++++++++ 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 5b6aed6110a..177c34b97e3 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -214,9 +214,11 @@ export default util.createRule({ function checkAlignGroup(group: TSESTree.Node[]): void { let alignColumn = 0; - const align = + const align: 'value' | 'colon' = (typeof options.align === 'object' ? options.align.on + : typeof options.multiLine?.align === 'object' + ? options.multiLine.align.on : options.multiLine?.align ?? options.align) ?? 'colon'; const beforeColon = (typeof options.align === 'object' @@ -261,35 +263,47 @@ export default util.createRule({ } for (const node of group) { - if (isKeyTypeNode(node)) { - const start = - align === 'colon' - ? node.typeAnnotation!.loc.start.column - : node.typeAnnotation!.typeAnnotation.loc.start.column; - - if (start !== alignColumn) { - context.report({ - node, - messageId: - start > alignColumn - ? align === 'colon' - ? 'extraKey' - : 'extraValue' - : align === 'colon' - ? 'missingKey' - : 'missingValue', - data: { - computed: '', - key: getKeyText(node), - }, - }); - } + if (!isKeyTypeNode(node)) { + continue; + } + const toCheck = + align === 'colon' + ? node.typeAnnotation! + : node.typeAnnotation!.typeAnnotation; + const difference = toCheck.loc.start.column - alignColumn; + + if (difference) { + context.report({ + node, + messageId: + difference > 0 + ? align === 'colon' + ? 'extraKey' + : 'extraValue' + : align === 'colon' + ? 'missingKey' + : 'missingValue', + fix: fixer => { + if (difference > 0) { + return fixer.removeRange([ + toCheck.range[0] - difference, + toCheck.range[0], + ]); + } else { + return fixer.insertTextBefore(toCheck, ' '.repeat(-difference)); + } + }, + data: { + computed: '', + key: getKeyText(node), + }, + }); + } - if (align === 'colon') { - checkAfterColon(node, nAfterColon, mode); - } else { - checkBeforeColon(node, nBeforeColon, mode); - } + if (align === 'colon') { + checkAfterColon(node, nAfterColon, mode); + } else { + checkBeforeColon(node, nBeforeColon, mode); } } } diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 49de9d0ef4c..9fbe24d6843 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -112,31 +112,37 @@ interface X { // align: value { code: 'interface X {\n a: number;\n abc: string\n};', + output: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { code: 'class X {\n a: number;\n abc: string\n};', + output: 'class X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { code: 'class X {\n a: number;\n abc: string\n};', + output: 'class X {\n a: number;\n abc: string\n};', options: [{ align: 'value', mode: 'minimum' }], errors: [{ messageId: 'missingValue' }], }, { code: 'type X = {\n a: number;\n abc: string\n};', + output: 'type X = {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { code: 'interface X {\n a: number;\n abc: string\n};', + output: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, { code: 'class X {\n a: number;\n abc: string\n};', + output: 'class X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, @@ -169,6 +175,17 @@ interface X { }, abc: string } +`, + output: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], @@ -184,6 +201,17 @@ interface X { }, abc: string } +`, + output: +` +interface X { + a: number; + prop: { + abc: number; + a: number; + }, + abc: string +} `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], @@ -217,6 +245,7 @@ interface X { // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', + output: 'interface X {\n a : number;\n abc: string\n};', options: [{ align: 'colon' }], errors: [{ messageId: 'extraKey' }], }, From bc3b5d27fa0b7c6bcce120acb3603b0c717417da Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 11:29:43 +0100 Subject: [PATCH 15/40] =?UTF-8?q?=E2=9C=8F=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/key-spacing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 177c34b97e3..7bd32517298 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -48,7 +48,7 @@ export default util.createRule({ /** * Relevant nodes to our rule * - * node.typeAnnotation will aways be defined, but no way to enforce that and keep + * node.typeAnnotation will always be defined, but no way to enforce that and keep * the type compatible with TSEstree.Node (except maybe TS 4.9' "satisfies" keyword) */ type KeyTypeNode = From eebb18f2933f70411a632062db4137a86eb95ee0 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 11:36:26 +0100 Subject: [PATCH 16/40] =?UTF-8?q?=F0=9F=9A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 9fbe24d6843..64e037b7336 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -53,8 +53,7 @@ ruleTester.run('key-spacing', rule, { options: [{ align: 'value', mode: 'minimum' }], }, { - code: -` + code: ` interface X { a: number; prop: { @@ -63,7 +62,7 @@ interface X { }, abc: string } -`, + `, options: [{ align: 'value' }], }, // align: colon @@ -160,13 +159,13 @@ interface X { }, { code: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', - output: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', + output: + 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, { - code: -` + code: ` interface X { a: number; prop: { @@ -175,9 +174,8 @@ interface X { }, abc: string } -`, - output: -` + `, + output: ` interface X { a: number; prop: { @@ -186,13 +184,12 @@ interface X { }, abc: string } -`, + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: -` + code: ` interface X { a: number; prop: { @@ -201,9 +198,8 @@ interface X { }, abc: string } -`, - output: -` + `, + output: ` interface X { a: number; prop: { @@ -212,13 +208,12 @@ interface X { }, abc: string } -`, + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: -` + code: ` interface X { a: number; prop: { @@ -227,9 +222,8 @@ interface X { }, abc: string } -`, - output: -` + `, + output: ` interface X { a: number; prop: { @@ -238,7 +232,7 @@ interface X { }, abc: string } -`, + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, From 179aca2a3cf5959a9759b4fbe9727185556ac150 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 11:45:48 +0100 Subject: [PATCH 17/40] =?UTF-8?q?=F0=9F=90=9B=20Support=20optional=20=3F?= =?UTF-8?q?=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/key-spacing.ts | 16 ++++------------ .../tests/rules/key-spacing.test.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 7bd32517298..ea192c3007f 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -77,25 +77,17 @@ export default util.createRule({ const lastParam = node.parameters[node.parameters.length - 1]; return code.slice( 0, - getLastTokenBeforeColon(lastParam).range[1] - node.range[0], + sourceCode.getTokenAfter(lastParam, util.isClosingBracketToken)! + .range[1] - node.range[0], ); } /** * To handle index signatures, be able to get the end position of the parameters */ - function getKeyLocEnd( - node: - | TSESTree.TSIndexSignature - | TSESTree.TSPropertySignature - | TSESTree.PropertyDefinition, - ): TSESTree.Position { - if ('key' in node) { - return node.key.loc.end; - } - + function getKeyLocEnd(node: KeyTypeNode): TSESTree.Position { return getLastTokenBeforeColon( - node.parameters[node.parameters.length - 1], + 'key' in node ? node.key : node.parameters[node.parameters.length - 1], ).loc.end; } diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 64e037b7336..8911bce8d48 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -16,6 +16,10 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'interface X {\n a?: number;\n abc: string\n};', + options: [{ align: 'value' }], + }, { code: 'interface X {\n a: number;\n // Some comment\n abc: string\n};', options: [{ align: 'value' }], @@ -36,6 +40,10 @@ ruleTester.run('key-spacing', rule, { code: 'class X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'class X {\n a?: number;\n abc: string\n};', + options: [{ align: 'value' }], + }, { code: 'class X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], From db2d1bd16bdab7e6541b3d2f6c2b013c5ea035ff Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 12:30:15 +0100 Subject: [PATCH 18/40] =?UTF-8?q?=E2=9C=85=20Add=20tests=20with=20class=20?= =?UTF-8?q?property=20assignments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 8911bce8d48..6a57edc5f39 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -44,6 +44,10 @@ ruleTester.run('key-spacing', rule, { code: 'class X {\n a?: number;\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', + options: [{ align: 'value' }], + }, { code: 'class X {\n a: number;\n\n abc: string\n};', options: [{ align: 'value' }], @@ -67,8 +71,25 @@ interface X { prop: { abc: number; a: number; - }, + }; + abc: string +} + `, + options: [{ align: 'value' }], + }, + { + code: ` +class X { + a: number; + prop: { + abc: number; + a: number; + }; abc: string + x = 1; + d: number; + z: number = 1; + ef: string; } `, options: [{ align: 'value' }], @@ -153,6 +174,12 @@ interface X { options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, + { + code: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', + output: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, { code: 'interface X {\n a: number;\n\n abc : string\n};', output: 'interface X {\n a: number;\n\n abc: string\n};', @@ -244,6 +271,44 @@ interface X { options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, + { + code: ` +class X { + a: number; + prop: { + abc: number; + a?: number; + }; + abc: string; + x = 1; + d: number; + z: number = 1; + ef: string; +} + `, + output: ` +class X { + a: number; + prop: { + abc: number; + a?: number; + }; + abc: string; + x = 1; + d: number; + z: number = 1; + ef: string; +} + `, + options: [{ align: 'value' }], + errors: [ + { messageId: 'extraValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + ], + }, // align: colon { code: 'interface X {\n a : number;\n abc: string\n};', From dbe502d16bb1785b6796ee7172ea0a61a917249d Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 12:44:48 +0100 Subject: [PATCH 19/40] =?UTF-8?q?=F0=9F=93=9D=20Add=20documentation=20on?= =?UTF-8?q?=20key-spacing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/docs/rules/key-spacing.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/key-spacing.md diff --git a/packages/eslint-plugin/docs/rules/key-spacing.md b/packages/eslint-plugin/docs/rules/key-spacing.md new file mode 100644 index 00000000000..16abf2eeba3 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/key-spacing.md @@ -0,0 +1,12 @@ +--- +description: 'Enforce consistent spacing after keys and before values / type annotations.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/key-spacing** for documentation. + +## Examples + +This rule extends the base [`eslint/keyword-spacing`](https://eslint.org/docs/rules/key-spacing) rule. +This version adds support for type annotations on interface, class and type literals properties. From 8d5d3ddb5a950a382e9c62ec1919c5cb01d04983 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 14:26:49 +0100 Subject: [PATCH 20/40] =?UTF-8?q?=F0=9F=8E=A8=20Use=20.at()=20to=20access?= =?UTF-8?q?=20last=20element=20of=20array?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/key-spacing.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index ea192c3007f..06c03678fe6 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -74,11 +74,12 @@ export default util.createRule({ } const code = sourceCode.getText(node); - const lastParam = node.parameters[node.parameters.length - 1]; return code.slice( 0, - sourceCode.getTokenAfter(lastParam, util.isClosingBracketToken)! - .range[1] - node.range[0], + sourceCode.getTokenAfter( + node.parameters.at(-1)!, + util.isClosingBracketToken, + )!.range[1] - node.range[0], ); } @@ -87,7 +88,7 @@ export default util.createRule({ */ function getKeyLocEnd(node: KeyTypeNode): TSESTree.Position { return getLastTokenBeforeColon( - 'key' in node ? node.key : node.parameters[node.parameters.length - 1], + 'key' in node ? node.key : node.parameters.at(-1)!, ).loc.end; } @@ -185,9 +186,7 @@ export default util.createRule({ if ( leadingComments.length && leadingComments[0].loc.start.line - groupEndLine <= 1 && - candidateValueStartLine - - leadingComments[leadingComments.length - 1].loc.end.line <= - 1 + candidateValueStartLine - leadingComments.at(-1)!.loc.end.line <= 1 ) { for (let i = 1; i < leadingComments.length; i++) { if ( @@ -355,9 +354,7 @@ export default util.createRule({ alignGroups.push(currentAlignGroup); for (const node of members) { - const prevNode = currentAlignGroup.length - ? currentAlignGroup[currentAlignGroup.length - 1] - : null; + const prevNode = currentAlignGroup.at(-1); if (prevNode && continuesAlignGroup(prevNode, node)) { currentAlignGroup.push(node); From af0a4891a4d4e3b59a728b876c75718259b4fe75 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Thu, 15 Dec 2022 23:15:21 +0100 Subject: [PATCH 21/40] =?UTF-8?q?=E2=9C=85=20Fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/docs/rules/key-spacing.md | 4 ++-- packages/eslint-plugin/src/configs/all.ts | 2 ++ packages/eslint-plugin/src/rules/key-spacing.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/key-spacing.md b/packages/eslint-plugin/docs/rules/key-spacing.md index 16abf2eeba3..3bfcf5f389f 100644 --- a/packages/eslint-plugin/docs/rules/key-spacing.md +++ b/packages/eslint-plugin/docs/rules/key-spacing.md @@ -1,5 +1,5 @@ --- -description: 'Enforce consistent spacing after keys and before values / type annotations.' +description: 'Enforce consistent spacing between property names and type annotations in types and interfaces.' --- > 🛑 This file is source code, not the primary documentation location! 🛑 @@ -9,4 +9,4 @@ description: 'Enforce consistent spacing after keys and before values / type ann ## Examples This rule extends the base [`eslint/keyword-spacing`](https://eslint.org/docs/rules/key-spacing) rule. -This version adds support for type annotations on interface, class and type literals properties. +This version adds support for type annotations on interfaces, classes and type literals properties. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 20ea892f581..452035c4ebf 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -37,6 +37,8 @@ export = { '@typescript-eslint/indent': 'error', 'init-declarations': 'off', '@typescript-eslint/init-declarations': 'error', + 'key-spacing': 'off', + '@typescript-eslint/key-spacing': 'error', 'keyword-spacing': 'off', '@typescript-eslint/keyword-spacing': 'error', 'lines-between-class-members': 'off', diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 06c03678fe6..d053c48d35c 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -20,7 +20,7 @@ export default util.createRule({ type: 'layout', docs: { description: - 'Enforce consistent spacing between keys and values in types and interfaces', + 'Enforce consistent spacing between property names and type annotations in types and interfaces.', recommended: false, extendsBaseRule: true, }, From c18b9291777360d1c7c8d86423b1c323a11ca525 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Fri, 16 Dec 2022 00:36:22 +0100 Subject: [PATCH 22/40] =?UTF-8?q?fixup!=20=E2=9C=85=20Fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/src/rules/key-spacing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index d053c48d35c..c9b71a91b06 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -20,7 +20,7 @@ export default util.createRule({ type: 'layout', docs: { description: - 'Enforce consistent spacing between property names and type annotations in types and interfaces.', + 'Enforce consistent spacing between property names and type annotations in types and interfaces', recommended: false, extendsBaseRule: true, }, From 37eecdc1c708f8381bb4af80730d3019ac447a5c Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Fri, 16 Dec 2022 03:41:44 +0100 Subject: [PATCH 23/40] =?UTF-8?q?=E2=9C=85=20Add=20some=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 2 +- .../tests/rules/key-spacing.test.ts | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index c9b71a91b06..4bd4421e976 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -245,7 +245,7 @@ export default util.createRule({ alignColumn, align === 'colon' ? getKeyLocEnd(node).column + nBeforeColon - : node.typeAnnotation!.loc.start.column + + : getKeyLocEnd(node).column + ':'.length + nAfterColon + nBeforeColon, diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 6a57edc5f39..69d12b273c3 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -16,6 +16,10 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a: number;\n abc: string\n};', options: [{ align: 'value' }], }, + { + code: 'interface X {\n a: number;\n abc: string; c: number;\n};', + options: [{ align: 'value' }], + }, { code: 'interface X {\n a?: number;\n abc: string\n};', options: [{ align: 'value' }], @@ -126,6 +130,51 @@ class X { }, ], }, + { + code: 'interface X {\n a : number;\n abc : string\n};', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: true, afterColon: true, align: 'value' }, + }, + ], + }, + { + code: 'interface X {\n a : number;\n abc : string\n};', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { + beforeColon: true, + afterColon: true, + align: { + on: 'colon', + mode: 'strict', + afterColon: true, + beforeColon: true, + }, + }, + }, + ], + }, + { + code: 'interface X {\n a : number;\n abc: string\n\n xadzd : number;\n};', + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { + beforeColon: true, + afterColon: true, + align: { + on: 'colon', + mode: 'strict', + afterColon: true, + beforeColon: false, + }, + }, + }, + ], + }, { code: 'interface X { a:number; abc:string; };', options: [ From 1b62445210f59205cdf3d6fa8911dd77dcc43961 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Fri, 16 Dec 2022 11:54:12 +0100 Subject: [PATCH 24/40] =?UTF-8?q?=F0=9F=90=9B=20Fix=20edge=20case=20in=20d?= =?UTF-8?q?etermining=20aligned=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case there is three statements in one line, and one statement in the line after --- packages/eslint-plugin/src/rules/key-spacing.ts | 13 ++++++++++--- .../eslint-plugin/tests/rules/key-spacing.test.ts | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 4bd4421e976..2112c5d59fa 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -353,13 +353,18 @@ export default util.createRule({ let currentAlignGroup: TSESTree.Node[] = []; alignGroups.push(currentAlignGroup); + let prevNode: TSESTree.Node | undefined = undefined; + for (const node of members) { - const prevNode = currentAlignGroup.at(-1); + let prevAlignedNode = currentAlignGroup.at(-1); + if (prevAlignedNode !== prevNode) { + prevAlignedNode = undefined; + } - if (prevNode && continuesAlignGroup(prevNode, node)) { + if (prevAlignedNode && continuesAlignGroup(prevAlignedNode, node)) { currentAlignGroup.push(node); } else if (prevNode?.loc.start.line === node.loc.start.line) { - if (currentAlignGroup.length) { + if (prevAlignedNode /* prevNode === prevAlignedNode */) { unalignedElements.push(currentAlignGroup.pop()!); } unalignedElements.push(node); @@ -367,6 +372,8 @@ export default util.createRule({ currentAlignGroup = [node]; alignGroups.push(currentAlignGroup); } + + prevNode = node; } unalignedElements.push( diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 69d12b273c3..3c60c9f8cae 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -20,6 +20,10 @@ ruleTester.run('key-spacing', rule, { code: 'interface X {\n a: number;\n abc: string; c: number;\n};', options: [{ align: 'value' }], }, + { + code: 'interface X {\n a: number;\n abc: string; c: number; de: boolean;\n abcef: number;\n};', + options: [{ align: 'colon' }], + }, { code: 'interface X {\n a?: number;\n abc: string\n};', options: [{ align: 'value' }], From 4b0759c04eac7f10b677806b0a085347ba3b1f94 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Fri, 16 Dec 2022 14:04:21 +0100 Subject: [PATCH 25/40] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Use=20Array.concat?= =?UTF-8?q?=20instead=20of=20.push(...)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .push could error if 60k + arguments --- packages/eslint-plugin/src/rules/key-spacing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 2112c5d59fa..7748b91cf4a 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -376,8 +376,8 @@ export default util.createRule({ prevNode = node; } - unalignedElements.push( - ...alignGroups + unalignedElements = unalignedElements.concat( + alignGroups .filter(group => group.length === 1) .flatMap(group => group), ); From 6d61144c2ece5a760ba08ef3c09299e71babc969 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 11:07:48 +0100 Subject: [PATCH 26/40] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 7748b91cf4a..7d5f931ba3e 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -94,12 +94,12 @@ export default util.createRule({ function checkBeforeColon( node: KeyTypeNode, - nBeforeColon: number, + expectedWhitespaceBeforeColon: number, mode: 'strict' | 'minimum', ): void { const colon = node.typeAnnotation!.loc.start.column; const keyEnd = getKeyLocEnd(node); - const difference = colon - keyEnd.column - nBeforeColon; + const difference = colon - keyEnd.column - expectedWhitespaceBeforeColon; if (mode === 'strict' ? difference : difference < 0) { context.report({ node, @@ -127,12 +127,12 @@ export default util.createRule({ function checkAfterColon( node: KeyTypeNode, - nAfterColon: number, + expectedWhitespaceAfterColon: number, mode: 'strict' | 'minimum', ): void { const colon = node.typeAnnotation!.loc.start.column; const typeStart = node.typeAnnotation!.typeAnnotation.loc.start.column; - const difference = typeStart - colon - 1 - nAfterColon; + const difference = typeStart - colon - 1 - expectedWhitespaceAfterColon; if (mode === 'strict' ? difference : difference < 0) { context.report({ node, @@ -219,7 +219,7 @@ export default util.createRule({ ? options.multiLine.align.beforeColon : options.multiLine.beforeColon : options.beforeColon) ?? false; - const nBeforeColon = beforeColon ? 1 : 0; + const expectedWhitespaceBeforeColon = beforeColon ? 1 : 0; const afterColon = (typeof options.align === 'object' ? options.align.afterColon @@ -228,7 +228,7 @@ export default util.createRule({ ? options.multiLine.align.afterColon : options.multiLine.afterColon : options.afterColon) ?? true; - const nAfterColon = afterColon ? 1 : 0; + const expectedWhitespaceAfterColon = afterColon ? 1 : 0; const mode = (typeof options.align === 'object' ? options.align.mode @@ -244,11 +244,11 @@ export default util.createRule({ alignColumn = Math.max( alignColumn, align === 'colon' - ? getKeyLocEnd(node).column + nBeforeColon + ? getKeyLocEnd(node).column + expectedWhitespaceBeforeColon : getKeyLocEnd(node).column + ':'.length + - nAfterColon + - nBeforeColon, + expectedWhitespaceAfterColon + + expectedWhitespaceBeforeColon, ); } } @@ -292,9 +292,9 @@ export default util.createRule({ } if (align === 'colon') { - checkAfterColon(node, nAfterColon, mode); + checkAfterColon(node, expectedWhitespaceAfterColon, mode); } else { - checkBeforeColon(node, nBeforeColon, mode); + checkBeforeColon(node, expectedWhitespaceBeforeColon, mode); } } } @@ -311,7 +311,7 @@ export default util.createRule({ : options.multiLine ? options.multiLine.beforeColon : options.beforeColon) ?? false; - const nBeforeColon = beforeColon ? 1 : 0; + const expectedWhitespaceBeforeColon = beforeColon ? 1 : 0; const afterColon = (singleLine ? options.singleLine @@ -320,7 +320,7 @@ export default util.createRule({ : options.multiLine ? options.multiLine.afterColon : options.afterColon) ?? true; - const nAfterColon = afterColon ? 1 : 0; + const expectedWhitespaceAfterColon = afterColon ? 1 : 0; const mode = (singleLine ? options.singleLine @@ -331,8 +331,8 @@ export default util.createRule({ : options.mode) ?? 'strict'; if (isKeyTypeNode(node)) { - checkBeforeColon(node, nBeforeColon, mode); - checkAfterColon(node, nAfterColon, mode); + checkBeforeColon(node, expectedWhitespaceBeforeColon, mode); + checkAfterColon(node, expectedWhitespaceAfterColon, mode); } } @@ -364,8 +364,10 @@ export default util.createRule({ if (prevAlignedNode && continuesAlignGroup(prevAlignedNode, node)) { currentAlignGroup.push(node); } else if (prevNode?.loc.start.line === node.loc.start.line) { - if (prevAlignedNode /* prevNode === prevAlignedNode */) { - unalignedElements.push(currentAlignGroup.pop()!); + if (prevAlignedNode) { + // Here, prevNode === prevAlignedNode === currentAlignGroup.at(-1) + unalignedElements.push(prevAlignedNode); + currentAlignGroup.pop(); } unalignedElements.push(node); } else { @@ -377,9 +379,7 @@ export default util.createRule({ } unalignedElements = unalignedElements.concat( - alignGroups - .filter(group => group.length === 1) - .flatMap(group => group), + ...alignGroups.filter(group => group.length === 1), ); alignGroups = alignGroups.filter(group => group.length >= 2); } else { From 47156ecedfa038eb346890677c7f3ffcc0655786 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 11:20:40 +0100 Subject: [PATCH 27/40] =?UTF-8?q?=F0=9F=8E=A8=20Use=20tempate=20literals?= =?UTF-8?q?=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 406 +++++++++++++++--- 1 file changed, 348 insertions(+), 58 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 3c60c9f8cae..53a0a92dff1 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -13,63 +13,149 @@ ruleTester.run('key-spacing', rule, { valid: [ // align: value { - code: 'interface X {\n a: number;\n abc: string\n};', + code: ` +interface X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n abc: string; c: number;\n};', + code: ` +interface X { + a: number; + abc: string; c: number; +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n abc: string; c: number; de: boolean;\n abcef: number;\n};', + code: ` +interface X { + a: number; + abc: string; c: number; de: boolean; + abcef: number; +}; + `, options: [{ align: 'colon' }], }, { - code: 'interface X {\n a?: number;\n abc: string\n};', + code: ` +interface X { + a?: number; + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n // Some comment\n abc: string\n};', + code: ` +interface X { + a: number; + // Some comment + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n // Some comment\n // on multiple lines\n abc: string\n};', + code: ` +interface X { + a: number; + // Some comment + // on multiple lines + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n /**\n * Doc comment\n */\n abc: string\n};', + code: ` +interface X { + a: number; + /** + * Doc comment + */ + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'interface X {\n a: number;\n\n abc: string\n};', + code: ` +interface X { + a: number; + + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'class X {\n a: number;\n abc: string\n};', + code: ` +class X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'class X {\n a?: number;\n abc: string\n};', + code: ` +class X { + a?: number; + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', + code: ` +class X { + x: number; + z = 1; + xbcef: number; + } + `, options: [{ align: 'value' }], }, { - code: 'class X {\n a: number;\n\n abc: string\n};', + code: ` +class X { + a: number; + + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'type X = {\n a: number;\n abc: string\n};', + code: ` +type X = { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'type X = {\n a: number;\n\n abc: string\n};', + code: ` +type X = { + a: number; + + abc: string +}; + `, options: [{ align: 'value' }], }, { - code: 'type X = {\n a : number;\n abc: string\n};', + code: ` +type X = { + a : number; + abc: string +}; + `, options: [{ align: 'value', mode: 'minimum' }], }, { @@ -104,29 +190,59 @@ class X { }, // align: colon { - code: 'interface X {\n a : number;\n abc: string\n};', + code: ` +interface X { + a : number; + abc: string +}; + `, options: [{ align: 'colon' }], }, { - code: 'interface X {\n a :number;\n abc:string\n};', + code: ` +interface X { + a :number; + abc:string +}; + `, options: [{ align: 'colon', afterColon: false }], }, { - code: 'interface X {\n a : number;\n abc: string\n};', + code: ` +interface X { + a : number; + abc: string +}; + `, options: [{ align: 'colon', mode: 'minimum' }], }, // no align { - code: 'interface X {\n a: number;\n abc: string\n};', + code: ` +interface X { + a: number; + abc: string +}; + `, options: [{}], }, { - code: 'interface X {\n a : number;\n abc : string\n};', + code: ` +interface X { + a : number; + abc : string +}; + `, options: [{ beforeColon: true }], }, // singleLine / multiLine { - code: 'interface X {\n a : number;\n abc : string\n};', + code: ` +interface X { + a : number; + abc : string +}; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -135,7 +251,12 @@ class X { ], }, { - code: 'interface X {\n a : number;\n abc : string\n};', + code: ` +interface X { + a : number; + abc : string +}; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -144,7 +265,12 @@ class X { ], }, { - code: 'interface X {\n a : number;\n abc : string\n};', + code: ` +interface X { + a : number; + abc : string +}; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -162,7 +288,14 @@ class X { ], }, { - code: 'interface X {\n a : number;\n abc: string\n\n xadzd : number;\n};', + code: ` +interface X { + a : number; + abc: string + + xadzd : number; +}; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -180,7 +313,9 @@ class X { ], }, { - code: 'interface X { a:number; abc:string; };', + code: ` +interface X { a:number; abc:string; }; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -192,63 +327,174 @@ class X { invalid: [ // align: value { - code: 'interface X {\n a: number;\n abc: string\n};', - output: 'interface X {\n a: number;\n abc: string\n};', + code: ` +interface X { + a: number; + abc: string +}; + `, + output: ` +interface X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: 'class X {\n a: number;\n abc: string\n};', - output: 'class X {\n a: number;\n abc: string\n};', + code: ` +class X { + a: number; + abc: string +}; + `, + output: ` +class X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: 'class X {\n a: number;\n abc: string\n};', - output: 'class X {\n a: number;\n abc: string\n};', + code: ` +class X { + a: number; + abc: string +}; + `, + output: ` +class X { + a: number; + abc: string +}; + `, options: [{ align: 'value', mode: 'minimum' }], errors: [{ messageId: 'missingValue' }], }, { - code: 'type X = {\n a: number;\n abc: string\n};', - output: 'type X = {\n a: number;\n abc: string\n};', + code: ` +type X = { + a: number; + abc: string +}; + `, + output: ` +type X = { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: 'interface X {\n a: number;\n abc: string\n};', - output: 'interface X {\n a: number;\n abc: string\n};', + code: ` +interface X { + a: number; + abc: string +}; + `, + output: ` +interface X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, { - code: 'class X {\n a: number;\n abc: string\n};', - output: 'class X {\n a: number;\n abc: string\n};', + code: ` +class X { + a: number; + abc: string +}; + `, + output: ` +class X { + a: number; + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, { - code: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', - output: 'class X {\n x: number;\n z = 1;\n xbcef: number;\n }', + code: ` +class X { + x: number; + z = 1; + xbcef: number; + } + `, + output: ` +class X { + x: number; + z = 1; + xbcef: number; + } + `, options: [{ align: 'value' }], errors: [{ messageId: 'missingValue' }], }, { - code: 'interface X {\n a: number;\n\n abc : string\n};', - output: 'interface X {\n a: number;\n\n abc: string\n};', + code: ` +interface X { + a: number; + + abc : string +}; + `, + output: ` +interface X { + a: number; + + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, { - code: 'class X {\n a: number;\n\n abc : string\n};', - output: 'class X {\n a: number;\n\n abc: string\n};', + code: ` +class X { + a: number; + + abc : string +}; + `, + output: ` +class X { + a: number; + + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }, { messageId: 'extraKey' }], }, { - code: 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', - output: - 'interface X {\n a: number;\n // Some comment\n\n // interrupted in the middle\n abc: string\n};', + code: ` +interface X { + a: number; + // Some comment + + // interrupted in the middle + abc: string +}; + `, + output: ` +interface X { + a: number; + // Some comment + + // interrupted in the middle + abc: string +}; + `, options: [{ align: 'value' }], errors: [{ messageId: 'extraValue' }], }, @@ -364,26 +610,62 @@ class X { }, // align: colon { - code: 'interface X {\n a : number;\n abc: string\n};', - output: 'interface X {\n a : number;\n abc: string\n};', + code: ` +interface X { + a : number; + abc: string +}; + `, + output: ` +interface X { + a : number; + abc: string +}; + `, options: [{ align: 'colon' }], errors: [{ messageId: 'extraKey' }], }, // no align { - code: 'interface X {\n [x: number]: string;\n}', - output: 'interface X {\n [x: number]: string;\n}', + code: ` +interface X { + [x: number]: string; +} + `, + output: ` +interface X { + [x: number]: string; +} + `, errors: [{ messageId: 'extraValue' }], }, { - code: 'interface X {\n [x: number]:string;\n}', - output: 'interface X {\n [x: number]: string;\n}', + code: ` +interface X { + [x: number]:string; +} + `, + output: ` +interface X { + [x: number]: string; +} + `, errors: [{ messageId: 'missingValue' }], }, // singleLine / multiLine { - code: 'interface X {\n a:number;\n abc:string\n};', - output: 'interface X {\n a : number;\n abc : string\n};', + code: ` +interface X { + a:number; + abc:string +}; + `, + output: ` +interface X { + a : number; + abc : string +}; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -398,8 +680,12 @@ class X { ], }, { - code: 'interface X { a : number; abc : string; };', - output: 'interface X { a:number; abc:string; };', + code: ` +interface X { a : number; abc : string; }; + `, + output: ` +interface X { a:number; abc:string; }; + `, options: [ { singleLine: { beforeColon: false, afterColon: false }, @@ -414,8 +700,12 @@ class X { ], }, { - code: 'interface X { a : number; abc : string; };', - output: 'interface X { a: number; abc: string; };', + code: ` +interface X { a : number; abc : string; }; + `, + output: ` +interface X { a: number; abc: string; }; + `, options: [ { singleLine: { beforeColon: false, afterColon: true }, From ae33b9b67c6a2a689c2bc3881ab2fc18b12c15fc Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 11:27:58 +0100 Subject: [PATCH 28/40] =?UTF-8?q?=E2=9C=85=20Add=20test=20with=20anonymous?= =?UTF-8?q?=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 53a0a92dff1..fdd6cfff448 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -23,6 +23,15 @@ interface X { }, { code: ` +let x: { + a: number; + abc: string +}; + `, + options: [{ align: 'value' }], + }, + { + code: ` interface X { a: number; abc: string; c: number; @@ -344,6 +353,22 @@ interface X { }, { code: ` +let x: { + a: number; + abc: string +}; + `, + output: ` +let x: { + a: number; + abc: string +}; + `, + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: ` class X { a: number; abc: string From 722e092641e550fe1ce03b143c9b7a2cf5f609c0 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 12:01:42 +0100 Subject: [PATCH 29/40] =?UTF-8?q?=E2=9C=85=20Add=20test=20with=20quoted=20?= =?UTF-8?q?keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index fdd6cfff448..8fc67861d30 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -23,6 +23,15 @@ interface X { }, { code: ` +interface X { + "a:b": number; + abcde: string +}; + `, + options: [{ align: 'value' }], + }, + { + code: ` let x: { a: number; abc: string @@ -353,6 +362,22 @@ interface X { }, { code: ` +interface X { + a: number; + "a:c": string +}; + `, + output: ` +interface X { + a: number; + "a:c": string +}; + `, + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: ` let x: { a: number; abc: string From 301731b3e645532be07255fad48c538eef9d0f4c Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 12:38:55 +0100 Subject: [PATCH 30/40] =?UTF-8?q?=E2=9E=95=20Add=20grapheme-splitter=20to?= =?UTF-8?q?=20deal=20with=20emojis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-plugin/package.json | 1 + .../eslint-plugin/src/rules/key-spacing.ts | 30 ++++++++++++-- .../tests/rules/key-spacing.test.ts | 40 +++++++++++++++++++ yarn.lock | 5 +++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 31cf59e366d..c83888a463a 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -49,6 +49,7 @@ "@typescript-eslint/utils": "5.46.1", "debug": "^4.3.4", "ignore": "^5.2.0", + "grapheme-splitter": "^1.0.4", "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 7d5f931ba3e..58cbc8d7a39 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -1,9 +1,24 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import GraphemeSplitter from 'grapheme-splitter'; import * as util from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; +let splitter: GraphemeSplitter; +function isASCII(value: string): boolean { + return /^[\u0020-\u007f]*$/u.test(value); +} +function getStringLength(value: string): number { + if (isASCII(value)) { + return value.length; + } + + splitter ??= new GraphemeSplitter(); + + return splitter.countGraphemes(value); +} + const baseRule = getESLintCoreRule('key-spacing'); export type Options = util.InferOptionsTypeFromRule; @@ -35,6 +50,14 @@ export default util.createRule({ const sourceCode = context.getSourceCode(); const baseRules = baseRule.create(context); + /** + * @returns the column of the position after converting all unicode characters in the line to 1 char length + */ + function adjustedColumn(position: TSESTree.Position): number { + const line = position.line - 1; // position.line is 1-indexed + return getStringLength(sourceCode.lines[line].slice(0, position.column)); + } + /** * Starting from the given a node (a property.key node here) looks forward * until it finds the last token before a colon punctuator and returns it. @@ -241,11 +264,12 @@ export default util.createRule({ for (const node of group) { if (isKeyTypeNode(node)) { + const keyEnd = adjustedColumn(getKeyLocEnd(node)); alignColumn = Math.max( alignColumn, align === 'colon' - ? getKeyLocEnd(node).column + expectedWhitespaceBeforeColon - : getKeyLocEnd(node).column + + ? keyEnd + expectedWhitespaceBeforeColon + : keyEnd + ':'.length + expectedWhitespaceAfterColon + expectedWhitespaceBeforeColon, @@ -261,7 +285,7 @@ export default util.createRule({ align === 'colon' ? node.typeAnnotation! : node.typeAnnotation!.typeAnnotation; - const difference = toCheck.loc.start.column - alignColumn; + const difference = adjustedColumn(toCheck.loc.start) - alignColumn; if (difference) { context.report({ diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 8fc67861d30..83b308c3008 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -41,6 +41,20 @@ let x: { }, { code: ` +let x: { + a: number; + "𐌘": string; + [𐌘]: Date; + "🌷": "bar", // 2 code points + "🎁": "baz", // 2 code points + "🇮🇳": "qux", // 4 code points + "🏳️‍🌈": "xyz", // 6 code points +}; + `, + options: [{ align: 'value' }], + }, + { + code: ` interface X { a: number; abc: string; c: number; @@ -394,6 +408,32 @@ let x: { }, { code: ` +let x: { + a: number; + "🌷": "bar", // 2 code points + "🎁": "baz", // 2 code points + "🇮🇳": "qux", // 4 code points + "🏳️‍🌈": "xyz", // 6 code points + [𐌘]: string + "𐌘": string +}; + `, + output: ` +let x: { + a: number; + "🌷": "bar", // 2 code points + "🎁": "baz", // 2 code points + "🇮🇳": "qux", // 4 code points + "🏳️‍🌈": "xyz", // 6 code points + [𐌘]: string + "𐌘": string +}; + `, + options: [{ align: 'value' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: ` class X { a: number; abc: string diff --git a/yarn.lock b/yarn.lock index 0c714b7280a..8186b40cd6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7908,6 +7908,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" From 9eaebb2c3fd397f7c2d1f933639783b98d1ad5c0 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 12:55:37 +0100 Subject: [PATCH 31/40] =?UTF-8?q?=E2=9C=85=20Add=20test=20case=20for=20mul?= =?UTF-8?q?tiline=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 83b308c3008..a6cdcfe2160 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -104,6 +104,19 @@ interface X { }, { code: ` +interface X { + a: number; + /** + * Some comment + * on multiple lines + */ + abc: string +}; + `, + options: [{ align: 'value' }], + }, + { + code: ` interface X { a: number; /** @@ -590,6 +603,32 @@ interface X { }, { code: ` +interface X { + a: number; + /** + * Multiline comment + */ + + /** interrupted in the middle */ + abc: string +}; + `, + output: ` +interface X { + a: number; + /** + * Multiline comment + */ + + /** interrupted in the middle */ + abc: string +}; + `, + options: [{ align: 'value' }], + errors: [{ messageId: 'extraValue' }], + }, + { + code: ` interface X { a: number; prop: { From ee6d9bc3476c33b87df8b5274f9fb74c7a1fb85d Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 14:55:09 +0100 Subject: [PATCH 32/40] =?UTF-8?q?=F0=9F=9A=A8=20Remove=20'in'=20statements?= =?UTF-8?q?,=20reduce=20amount=20of=20null-assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eslint-plugin/src/rules/key-spacing.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index 58cbc8d7a39..a67dfd22482 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -92,7 +92,7 @@ export default util.createRule({ * To handle index signatures, to get the whole text for the parameters */ function getKeyText(node: KeyTypeNode): string { - if ('key' in node) { + if (node.type !== AST_NODE_TYPES.TSIndexSignature) { return sourceCode.getText(node.key); } @@ -111,7 +111,9 @@ export default util.createRule({ */ function getKeyLocEnd(node: KeyTypeNode): TSESTree.Position { return getLastTokenBeforeColon( - 'key' in node ? node.key : node.parameters.at(-1)!, + node.type !== AST_NODE_TYPES.TSIndexSignature + ? node.key + : node.parameters.at(-1)!, ).loc.end; } @@ -120,7 +122,9 @@ export default util.createRule({ expectedWhitespaceBeforeColon: number, mode: 'strict' | 'minimum', ): void { - const colon = node.typeAnnotation!.loc.start.column; + // KeyTypeNode always has type annotation + const typeAnnotation = node.typeAnnotation!; + const colon = typeAnnotation.loc.start.column; const keyEnd = getKeyLocEnd(node); const difference = colon - keyEnd.column - expectedWhitespaceBeforeColon; if (mode === 'strict' ? difference : difference < 0) { @@ -130,12 +134,12 @@ export default util.createRule({ fix: fixer => { if (difference > 0) { return fixer.removeRange([ - node.typeAnnotation!.range[0] - difference, - node.typeAnnotation!.range[0], + typeAnnotation.range[0] - difference, + typeAnnotation.range[0], ]); } else { return fixer.insertTextBefore( - node.typeAnnotation!, + typeAnnotation, ' '.repeat(-difference), ); } @@ -153,8 +157,10 @@ export default util.createRule({ expectedWhitespaceAfterColon: number, mode: 'strict' | 'minimum', ): void { - const colon = node.typeAnnotation!.loc.start.column; - const typeStart = node.typeAnnotation!.typeAnnotation.loc.start.column; + // KeyTypeNode always has type annotation + const typeAnnotation = node.typeAnnotation!; + const colon = typeAnnotation.loc.start.column; + const typeStart = typeAnnotation.typeAnnotation.loc.start.column; const difference = typeStart - colon - 1 - expectedWhitespaceAfterColon; if (mode === 'strict' ? difference : difference < 0) { context.report({ @@ -163,12 +169,12 @@ export default util.createRule({ fix: fixer => { if (difference > 0) { return fixer.removeRange([ - node.typeAnnotation!.typeAnnotation.range[0] - difference, - node.typeAnnotation!.typeAnnotation.range[0], + typeAnnotation.typeAnnotation.range[0] - difference, + typeAnnotation.typeAnnotation.range[0], ]); } else { return fixer.insertTextBefore( - node.typeAnnotation!.typeAnnotation, + typeAnnotation.typeAnnotation, ' '.repeat(-difference), ); } @@ -281,10 +287,10 @@ export default util.createRule({ if (!isKeyTypeNode(node)) { continue; } + // KeyTypeNode always has type annotation + const typeAnnotation = node.typeAnnotation!; const toCheck = - align === 'colon' - ? node.typeAnnotation! - : node.typeAnnotation!.typeAnnotation; + align === 'colon' ? typeAnnotation : typeAnnotation.typeAnnotation; const difference = adjustedColumn(toCheck.loc.start) - alignColumn; if (difference) { @@ -368,7 +374,8 @@ export default util.createRule({ ): void { const isSingleLine = body.loc.start.line === body.loc.end.line; - const members = 'members' in body ? body.members : body.body; + const members = + body.type === AST_NODE_TYPES.TSTypeLiteral ? body.members : body.body; let alignGroups: TSESTree.Node[][] = []; let unalignedElements: TSESTree.Node[] = []; From dbc6a91963c3148f3e74a921f1a5c3033808adf3 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 15:04:54 +0100 Subject: [PATCH 33/40] =?UTF-8?q?=E2=9C=85=20Add=20test=20case=20for=20pro?= =?UTF-8?q?perties=20without=20type=20annotation=20or=20assignments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index a6cdcfe2160..a1a2843b1db 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -74,6 +74,16 @@ interface X { }, { code: ` +interface X { + a : number; + abc; + abcef: number; +}; + `, + options: [{ align: 'colon' }], + }, + { + code: ` interface X { a?: number; abc: string @@ -479,6 +489,24 @@ class X { }, { code: ` +class X { + a: number; + b; + abc: string +}; + `, + output: ` +class X { + a: number; + b; + abc: string +}; + `, + options: [{ align: 'value', mode: 'minimum' }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: ` type X = { a: number; abc: string From b1033ef7b002381b56dfd7f6d6de47d619d5bd42 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 15:32:52 +0100 Subject: [PATCH 34/40] =?UTF-8?q?=E2=9C=85=20Add=20wacky=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index a1a2843b1db..1e35608c0b0 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -871,5 +871,121 @@ interface X { a: number; abc: string; }; ], errors: [{ messageId: 'extraKey' }, { messageId: 'extraKey' }], }, + { + code: ` +type Wacky = { + a: number; + b: string; + agc: number; + middle: Date | { + inner: { + a: boolean; + bc: boolean; + "🌷": "rose"; + } + [x: number]: string; + abc: boolean; + } +} & { + a: "string"; + abc: number; +} + `, + output: ` +type Wacky = { + a: number; + b: string; + agc: number; + middle: Date | { + inner: { + a: boolean; + bc: boolean; + "🌷": "rose"; + } + [x: number]: string; + abc: boolean; + } +} & { + a: "string"; + abc: number; +} + `, + options: [{ align: 'value' }], + errors: [ + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + ], + }, + { + code: ` +class Wacky { + a: number; + b?: string; + public z: number; + abc = 10; + private override xy: number; + static x = "test"; + static abcdef: number = 1; + get fn(): number { return 0; }; + inter: number; + get fn2(): number { + return 1; + }; + agc: number; + middle: Date | { + inner: { + a: boolean; + bc: boolean; + "🌷": "rose"; + } + [x: number]: string; + abc: boolean; + } +} + `, + output: ` +class Wacky { + a: number; + b?: string; + public z: number; + abc = 10; + private override xy: number; + static x = "test"; + static abcdef: number = 1; + get fn(): number { return 0; }; + inter: number; + get fn2(): number { + return 1; + }; + agc: number; + middle: Date | { + inner: { + a: boolean; + bc: boolean; + "🌷": "rose"; + } + [x: number]: string; + abc: boolean; + } +} + `, + options: [{ align: 'value' }], + errors: [ + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + { messageId: 'missingValue' }, + ], + }, ], }); From a7a5b2c39ae23847724da828434b992da46a2509 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 16:00:29 +0100 Subject: [PATCH 35/40] =?UTF-8?q?=E2=9C=85=20Add=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 1e35608c0b0..716fc8ea48f 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -344,6 +344,26 @@ interface X { }, { code: ` +interface X { + a : number; + abc : string +}; + `, + options: [ + { + beforeColon: true, + afterColon: true, + align: { + on: 'colon', + mode: 'strict', + afterColon: true, + beforeColon: true, + }, + }, + ], + }, + { + code: ` interface X { a : number; abc: string @@ -782,6 +802,22 @@ interface X { options: [{ align: 'colon' }], errors: [{ messageId: 'extraKey' }], }, + { + code: ` +interface X { + a : number; + abc: string +}; + `, + output: ` +interface X { + a : number; + abc : string +}; + `, + options: [{ align: 'colon', beforeColon: true, afterColon: true }], + errors: [{ messageId: 'missingKey' }], + }, // no align { code: ` @@ -873,6 +909,86 @@ interface X { a: number; abc: string; }; }, { code: ` +interface X { a:number; abc:string; }; + `, + output: ` +interface X { a : number; abc : string; }; + `, + options: [ + { + singleLine: { beforeColon: true, afterColon: true, mode: 'strict' }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + errors: [ + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + ], + }, + { + code: ` +interface X { a:number; abc: string; }; + `, + output: ` +interface X { a : number; abc : string; }; + `, + options: [ + { + singleLine: { beforeColon: true, afterColon: true, mode: 'minimum' }, + multiLine: { beforeColon: true, afterColon: true }, + }, + ], + errors: [ + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + { messageId: 'missingKey' }, + ], + }, + { + code: ` +interface X { a : number; abc : string; }; + `, + output: ` +interface X { a:number; abc:string; }; + `, + options: [ + { + beforeColon: false, + afterColon: false, + }, + ], + errors: [ + { messageId: 'extraKey' }, + { messageId: 'extraValue' }, + { messageId: 'extraKey' }, + { messageId: 'extraValue' }, + ], + }, + { + code: ` +interface X { a:number; abc:string; }; + `, + output: ` +interface X { a : number; abc : string; }; + `, + options: [ + { + beforeColon: true, + afterColon: true, + mode: 'strict', + }, + ], + errors: [ + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + { messageId: 'missingKey' }, + { messageId: 'missingValue' }, + ], + }, + { + code: ` type Wacky = { a: number; b: string; From cb5876e2bd44f06a6a01a67be42f7ef79951482b Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 16:42:05 +0100 Subject: [PATCH 36/40] =?UTF-8?q?=E2=9C=85=20Add=20coverage,=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index 716fc8ea48f..d3c96d293f9 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -215,6 +215,24 @@ type X = { }, { code: ` +type X = { + a : number; + abc: string +}; + `, + options: [ + { + align: { + on: 'value', + mode: 'minimum', + beforeColon: false, + afterColon: true, + }, + }, + ], + }, + { + code: ` interface X { a: number; prop: { @@ -389,6 +407,56 @@ interface X { }, { code: ` +interface X { + a : number; + abc: string + + xadzd : number; +}; + `, + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { + beforeColon: true, + afterColon: true, + mode: 'strict', + align: { + on: 'colon', + afterColon: true, + beforeColon: false, + }, + }, + }, + ], + }, + { + code: ` +interface X { + a : number; + abc: string + + xadzd : number; +}; + `, + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { + beforeColon: true, + afterColon: true, + mode: 'minimum', + align: { + on: 'colon', + afterColon: true, + beforeColon: false, + }, + }, + }, + ], + }, + { + code: ` interface X { a:number; abc:string; }; `, options: [ From 27877629bbbd62dbbf7871a7dded5d6ce48281f9 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 17:08:54 +0100 Subject: [PATCH 37/40] =?UTF-8?q?=E2=9C=85=20Add=20coverage,=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/rules/key-spacing.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index d3c96d293f9..d9aaf562c9d 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -328,6 +328,21 @@ interface X { interface X { a : number; abc : string +}; + `, + options: [ + { + align: { on: 'value', beforeColon: true, afterColon: true }, + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: false, afterColon: false }, + }, + ], + }, + { + code: ` +interface X { + a : number; + abc : string }; `, options: [ @@ -519,6 +534,22 @@ let x: { }, { code: ` +let x: { + a: number; + abc: string +}; + `, + output: ` +let x: { + a: number; + abc: string +}; + `, + options: [{ align: { on: 'value' } }], + errors: [{ messageId: 'missingValue' }], + }, + { + code: ` let x: { a: number; "🌷": "bar", // 2 code points @@ -878,6 +909,22 @@ interface X { }; `, output: ` +interface X { + a : number; + abc: string +}; + `, + options: [{ align: { on: 'colon' } }], + errors: [{ messageId: 'extraKey' }], + }, + { + code: ` +interface X { + a : number; + abc: string +}; + `, + output: ` interface X { a : number; abc : string From 79d8ec0471f9764546c43ad6a0c446628409e679 Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Tue, 24 Jan 2023 17:47:54 +0100 Subject: [PATCH 38/40] =?UTF-8?q?=E2=9C=85=20Add=20coverage=20when=20align?= =?UTF-8?q?=20is=20an=20object,=20but=20align.on=20is=20missing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It defaults to 'colon' in this case --- .../tests/rules/key-spacing.test.ts | 56 +++++++++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 4 +- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/key-spacing.test.ts b/packages/eslint-plugin/tests/rules/key-spacing.test.ts index d9aaf562c9d..40206258671 100644 --- a/packages/eslint-plugin/tests/rules/key-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/key-spacing.test.ts @@ -340,6 +340,21 @@ interface X { }, { code: ` +interface X { + a : number; + abc : string +}; + `, + options: [ + { + align: { beforeColon: true, afterColon: true }, // defaults to 'colon' + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { beforeColon: false, afterColon: false }, + }, + ], + }, + { + code: ` interface X { a : number; abc : string @@ -380,6 +395,28 @@ interface X { interface X { a : number; abc : string +}; + `, + options: [ + { + singleLine: { beforeColon: false, afterColon: false }, + multiLine: { + beforeColon: true, + afterColon: true, + align: { + mode: 'strict', + afterColon: true, + beforeColon: true, + }, + }, + }, + ], + }, + { + code: ` +interface X { + a : number; + abc : string }; `, options: [ @@ -397,6 +434,25 @@ interface X { }, { code: ` +interface X { + a : number; + abc : string +}; + `, + options: [ + { + beforeColon: true, + afterColon: true, + align: { + mode: 'strict', + afterColon: true, + beforeColon: true, + }, + }, + ], + }, + { + code: ` interface X { a : number; abc: string diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 7eda886c91b..38682f60c5b 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -154,7 +154,7 @@ declare module 'eslint/lib/rules/key-spacing' { | 'value' | 'colon' | { - on: 'value' | 'colon'; + on?: 'value' | 'colon'; beforeColon?: boolean; afterColon?: boolean; mode?: 'strict' | 'minimum'; @@ -172,7 +172,7 @@ declare module 'eslint/lib/rules/key-spacing' { | 'value' | 'colon' | { - on: 'value' | 'colon'; + on?: 'value' | 'colon'; beforeColon?: boolean; afterColon?: boolean; mode?: 'strict' | 'minimum'; From 2092487bb9d6856bbd73611bd7a15099b5c5775a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 24 Jan 2023 17:01:52 -0500 Subject: [PATCH 39/40] KeyTypeNodeWithTypeAnnotation --- .../eslint-plugin/src/rules/key-spacing.ts | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index a67dfd22482..dfd726708c3 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -68,18 +68,18 @@ export default util.createRule({ return sourceCode.getTokenBefore(colonToken)!; } - /** - * Relevant nodes to our rule - * - * node.typeAnnotation will always be defined, but no way to enforce that and keep - * the type compatible with TSEstree.Node (except maybe TS 4.9' "satisfies" keyword) - */ type KeyTypeNode = | TSESTree.TSIndexSignature | TSESTree.TSPropertySignature | TSESTree.PropertyDefinition; - function isKeyTypeNode(node: TSESTree.Node): node is KeyTypeNode { + type KeyTypeNodeWithTypeAnnotation = KeyTypeNode & { + typeAnnotation: TSESTree.TSTypeAnnotation; + }; + + function isKeyTypeNode( + node: TSESTree.Node, + ): node is KeyTypeNodeWithTypeAnnotation { return ( (node.type === AST_NODE_TYPES.TSPropertySignature || node.type === AST_NODE_TYPES.TSIndexSignature || @@ -91,7 +91,7 @@ export default util.createRule({ /** * To handle index signatures, to get the whole text for the parameters */ - function getKeyText(node: KeyTypeNode): string { + function getKeyText(node: KeyTypeNodeWithTypeAnnotation): string { if (node.type !== AST_NODE_TYPES.TSIndexSignature) { return sourceCode.getText(node.key); } @@ -109,7 +109,9 @@ export default util.createRule({ /** * To handle index signatures, be able to get the end position of the parameters */ - function getKeyLocEnd(node: KeyTypeNode): TSESTree.Position { + function getKeyLocEnd( + node: KeyTypeNodeWithTypeAnnotation, + ): TSESTree.Position { return getLastTokenBeforeColon( node.type !== AST_NODE_TYPES.TSIndexSignature ? node.key @@ -118,12 +120,11 @@ export default util.createRule({ } function checkBeforeColon( - node: KeyTypeNode, + node: KeyTypeNodeWithTypeAnnotation, expectedWhitespaceBeforeColon: number, mode: 'strict' | 'minimum', ): void { - // KeyTypeNode always has type annotation - const typeAnnotation = node.typeAnnotation!; + const { typeAnnotation } = node; const colon = typeAnnotation.loc.start.column; const keyEnd = getKeyLocEnd(node); const difference = colon - keyEnd.column - expectedWhitespaceBeforeColon; @@ -153,12 +154,11 @@ export default util.createRule({ } function checkAfterColon( - node: KeyTypeNode, + node: KeyTypeNodeWithTypeAnnotation, expectedWhitespaceAfterColon: number, mode: 'strict' | 'minimum', ): void { - // KeyTypeNode always has type annotation - const typeAnnotation = node.typeAnnotation!; + const { typeAnnotation } = node; const colon = typeAnnotation.loc.start.column; const typeStart = typeAnnotation.typeAnnotation.loc.start.column; const difference = typeStart - colon - 1 - expectedWhitespaceAfterColon; @@ -194,7 +194,7 @@ export default util.createRule({ ): boolean { const groupEndLine = lastMember.loc.start.line; const candidateValueStartLine = ( - isKeyTypeNode(candidate) ? candidate.typeAnnotation! : candidate + isKeyTypeNode(candidate) ? candidate.typeAnnotation : candidate ).loc.start.line; if (candidateValueStartLine === groupEndLine) { @@ -287,8 +287,7 @@ export default util.createRule({ if (!isKeyTypeNode(node)) { continue; } - // KeyTypeNode always has type annotation - const typeAnnotation = node.typeAnnotation!; + const { typeAnnotation } = node; const toCheck = align === 'colon' ? typeAnnotation : typeAnnotation.typeAnnotation; const difference = adjustedColumn(toCheck.loc.start) - alignColumn; From 0184de977a8b59d794dcdc89796cdbb784a4ab4a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 24 Jan 2023 18:05:20 -0500 Subject: [PATCH 40/40] Extract to shared helper --- .../eslint-plugin/src/rules/ban-ts-comment.ts | 18 ++---------------- .../eslint-plugin/src/rules/key-spacing.ts | 19 +++---------------- .../eslint-plugin/src/util/getStringLength.ts | 17 +++++++++++++++++ packages/eslint-plugin/src/util/index.ts | 1 + 4 files changed, 23 insertions(+), 32 deletions(-) create mode 100644 packages/eslint-plugin/src/util/getStringLength.ts diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index f0bfce93ff6..511a951280e 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -1,22 +1,7 @@ import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; -import GraphemeSplitter from 'grapheme-splitter'; import * as util from '../util'; -let splitter: GraphemeSplitter; -function isASCII(value: string): boolean { - return /^[\u0020-\u007f]*$/u.test(value); -} -function getStringLength(value: string): number { - if (isASCII(value)) { - return value.length; - } - - splitter ??= new GraphemeSplitter(); - - return splitter.countGraphemes(value); -} - type DirectiveConfig = | boolean | 'allow-with-description' @@ -163,7 +148,8 @@ export default util.createRule<[Options], MessageIds>({ } = options; const format = descriptionFormats.get(fullDirective); if ( - getStringLength(description.trim()) < minimumDescriptionLength + util.getStringLength(description.trim()) < + minimumDescriptionLength ) { context.report({ data: { directive, minimumDescriptionLength }, diff --git a/packages/eslint-plugin/src/rules/key-spacing.ts b/packages/eslint-plugin/src/rules/key-spacing.ts index dfd726708c3..587d2674f4f 100644 --- a/packages/eslint-plugin/src/rules/key-spacing.ts +++ b/packages/eslint-plugin/src/rules/key-spacing.ts @@ -1,24 +1,9 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import GraphemeSplitter from 'grapheme-splitter'; import * as util from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; -let splitter: GraphemeSplitter; -function isASCII(value: string): boolean { - return /^[\u0020-\u007f]*$/u.test(value); -} -function getStringLength(value: string): number { - if (isASCII(value)) { - return value.length; - } - - splitter ??= new GraphemeSplitter(); - - return splitter.countGraphemes(value); -} - const baseRule = getESLintCoreRule('key-spacing'); export type Options = util.InferOptionsTypeFromRule; @@ -55,7 +40,9 @@ export default util.createRule({ */ function adjustedColumn(position: TSESTree.Position): number { const line = position.line - 1; // position.line is 1-indexed - return getStringLength(sourceCode.lines[line].slice(0, position.column)); + return util.getStringLength( + sourceCode.lines[line].slice(0, position.column), + ); } /** diff --git a/packages/eslint-plugin/src/util/getStringLength.ts b/packages/eslint-plugin/src/util/getStringLength.ts new file mode 100644 index 00000000000..65a22551949 --- /dev/null +++ b/packages/eslint-plugin/src/util/getStringLength.ts @@ -0,0 +1,17 @@ +import GraphemeSplitter from 'grapheme-splitter'; + +let splitter: GraphemeSplitter; + +function isASCII(value: string): boolean { + return /^[\u0020-\u007f]*$/u.test(value); +} + +export function getStringLength(value: string): number { + if (isASCII(value)) { + return value.length; + } + + splitter ??= new GraphemeSplitter(); + + return splitter.countGraphemes(value); +} diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index b2ad2927773..53a19a96d36 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -5,6 +5,7 @@ export * from './collectUnusedVariables'; export * from './createRule'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; +export * from './getStringLength'; export * from './getThisExpression'; export * from './getWrappingFixer'; export * from './isNodeEqual';