Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): additional annotation spacing rules for variables, parameters, properties, return types #1496

Merged
merged 9 commits into from Mar 2, 2020
Expand Up @@ -41,7 +41,7 @@ This rule has an object option:
- `"before": true`, (default for arrow) requires a space before the colon/arrow.
- `"after": true`, (default) requires a space after the colon/arrow.
- `"after": false`, disallows spaces after the colon/arrow.
- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`).
- `"overrides"`, overrides the default options for type annotations with `colon` (e.g. `const foo: string`) and function types with `arrow` (e.g. `type Foo = () => {}`). Additionally allows granular overrides for `variable` (`const foo: string`),`parameter` (`function foo(bar: string) {...}`),`property` (`interface Foo { bar: string }`) and `returnType` (`function foo(): string {...}`) annotations.

### defaults

Expand Down
172 changes: 139 additions & 33 deletions packages/eslint-plugin/src/rules/type-annotation-spacing.ts
@@ -1,22 +1,37 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

type Options = [
{
before?: boolean;
after?: boolean;
overrides?: {
colon?: {
before?: boolean;
after?: boolean;
};
arrow?: {
before?: boolean;
after?: boolean;
};
};
}?,
];
interface WhitespaceRule {
readonly before?: boolean;
readonly after?: boolean;
}

interface WhitespaceOverride {
readonly colon?: WhitespaceRule;
readonly arrow?: WhitespaceRule;
readonly variable?: WhitespaceRule;
readonly property?: WhitespaceRule;
readonly parameter?: WhitespaceRule;
readonly returnType?: WhitespaceRule;
}

interface Config extends WhitespaceRule {
readonly overrides?: WhitespaceOverride;
}

interface WhitespaceRules {
colon: WhitespaceRule;
arrow: WhitespaceRule;
variable: WhitespaceRule;
parameter: WhitespaceRule;
property: WhitespaceRule;
returnType: WhitespaceRule;
}
bradzacher marked this conversation as resolved.
Show resolved Hide resolved

type Options = [Config?];
type MessageIds =
| 'expectedSpaceAfter'
| 'expectedSpaceBefore'
Expand All @@ -32,6 +47,107 @@ const definition = {
additionalProperties: false,
};

function createRules(options?: Config): WhitespaceRules {
const globals = {
...(options?.before !== undefined ? { before: options.before } : {}),
...(options?.after !== undefined ? { after: options.after } : {}),
};
const override = options?.overrides ?? {};
const colon = {
...{ before: false, after: true },
...globals,
...override?.colon,
};
const arrow = {
...{ before: true, after: true },
...globals,
...override?.arrow,
};

return {
colon: colon,
arrow: arrow,
variable: { ...colon, ...override?.variable },
property: { ...colon, ...override?.property },
parameter: { ...colon, ...override?.parameter },
returnType: { ...colon, ...override?.returnType },
};
}

function getNodeType(node?: TSESTree.Node): string {
return node?.type ?? '';
}

function typeIsArrowFunction(type: string): boolean {
return type === AST_NODE_TYPES.TSFunctionType;
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function typeIsIdentifier(type: string): boolean {
return type === AST_NODE_TYPES.Identifier;
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function typeIsProperty(type: string): boolean {
return [
AST_NODE_TYPES.ClassProperty,
AST_NODE_TYPES.FunctionExpression,
AST_NODE_TYPES.TSPropertySignature,
AST_NODE_TYPES.TSMethodSignature,
].includes(type as AST_NODE_TYPES);
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function typeIsReturnType(type: string): boolean {
return [
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.ArrowFunctionExpression,
].includes(type as AST_NODE_TYPES);
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function typeIsVariable(type: string): boolean {
return type === AST_NODE_TYPES.VariableDeclarator;
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function typeIsParameter(type: string): boolean {
return [
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.TSFunctionType,
AST_NODE_TYPES.TSMethodSignature,
AST_NODE_TYPES.TSEmptyBodyFunctionExpression,
].includes(type as AST_NODE_TYPES);
}
chkt marked this conversation as resolved.
Show resolved Hide resolved

function getIdentifierRules(
rules: WhitespaceRules,
node?: TSESTree.Node,
chkt marked this conversation as resolved.
Show resolved Hide resolved
): WhitespaceRule {
const scope = node?.parent;
const type = getNodeType(scope);

if (typeIsVariable(type)) {
return rules.variable;
} else if (typeIsParameter(type)) {
return rules.parameter;
} else {
return rules.colon;
}
}

function getRules(rules: WhitespaceRules, node: TSESTree.Node): WhitespaceRule {
chkt marked this conversation as resolved.
Show resolved Hide resolved
const scope = node?.parent?.parent;
const type = getNodeType(scope);

if (typeIsArrowFunction(type)) {
return rules.arrow;
} else if (typeIsIdentifier(type)) {
return getIdentifierRules(rules, scope);
} else if (typeIsProperty(type)) {
return rules.property;
} else if (typeIsReturnType(type)) {
return rules.returnType;
} else {
return rules.colon;
}
}

export default util.createRule<Options, MessageIds>({
name: 'type-annotation-spacing',
meta: {
Expand Down Expand Up @@ -59,6 +175,10 @@ export default util.createRule<Options, MessageIds>({
properties: {
colon: definition,
arrow: definition,
variable: definition,
parameter: definition,
property: definition,
returnType: definition,
},
additionalProperties: false,
},
Expand All @@ -76,20 +196,7 @@ export default util.createRule<Options, MessageIds>({
const punctuators = [':', '=>'];
const sourceCode = context.getSourceCode();

const overrides = options?.overrides ?? { colon: {}, arrow: {} };

const colonOptions = Object.assign(
{},
{ before: false, after: true },
options,
overrides.colon,
);
const arrowOptions = Object.assign(
{},
{ before: true, after: true },
options,
overrides.arrow,
);
const ruleSet = createRules(options);

/**
* Checks if there's proper spacing around type annotations (no space
Expand All @@ -108,8 +215,7 @@ export default util.createRule<Options, MessageIds>({
return;
}

const before = type === ':' ? colonOptions.before : arrowOptions.before;
const after = type === ':' ? colonOptions.after : arrowOptions.after;
const { before, after } = getRules(ruleSet, typeAnnotation);

if (type === ':' && previousToken.value === '?') {
// shift the start to the ?
Expand Down
140 changes: 140 additions & 0 deletions packages/eslint-plugin/tests/rules/type-annotation-spacing.test.ts
Expand Up @@ -1034,6 +1034,146 @@ type Bar = Record<keyof Foo, string>
],
},
'let resolver: (() => PromiseLike<T>) | PromiseLike<T>;',
{
code: 'const foo:string;',
options: [
{
overrides: {
colon: {
after: false,
before: true,
},
variable: {
before: false,
},
},
},
],
},
{
code: 'const foo:string;',
options: [
{
before: true,
overrides: {
colon: {
after: true,
before: false,
},
variable: {
after: false,
},
},
},
],
},
{
code: `
interface Foo {
greet():string;
}
`,
options: [
{
overrides: {
colon: {
after: false,
before: true,
},
property: {
before: false,
},
},
},
],
},
{
code: `
interface Foo {
name:string;
}
`,
options: [
{
before: true,
overrides: {
colon: {
after: true,
before: false,
},
property: {
after: false,
},
},
},
],
},
{
code: 'function foo(name:string) {}',
options: [
{
overrides: {
colon: {
after: false,
before: true,
},
parameter: {
before: false,
},
},
},
],
},
{
code: 'function foo(name:string) {}',
options: [
{
before: true,
overrides: {
colon: {
after: true,
before: false,
},
parameter: {
after: false,
},
},
},
],
},
{
code: 'function foo():string {}',
options: [
{
overrides: {
colon: {
after: false,
before: true,
},
returnType: {
before: false,
},
},
},
],
},
{
code: 'function foo():string {}',
options: [
{
before: true,
overrides: {
colon: {
after: true,
before: false,
},
returnType: {
after: false,
},
},
},
],
},
],
invalid: [
{
Expand Down