Skip to content

Commit

Permalink
feat: add new rule sort-type-union-intersection-members (#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
Geraint White authored Sep 14, 2021
1 parent 4265b27 commit fa4207d
Show file tree
Hide file tree
Showing 6 changed files with 436 additions and 0 deletions.
1 change: 1 addition & 0 deletions .README/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d
{"gitdown": "include", "file": "./rules/require-variable-type.md"}
{"gitdown": "include", "file": "./rules/semi.md"}
{"gitdown": "include", "file": "./rules/sort-keys.md"}
{"gitdown": "include", "file": "./rules/sort-type-union-intersection-members.md"}
{"gitdown": "include", "file": "./rules/space-after-type-colon.md"}
{"gitdown": "include", "file": "./rules/space-before-generic-bracket.md"}
{"gitdown": "include", "file": "./rules/space-before-type-colon.md"}
Expand Down
101 changes: 101 additions & 0 deletions .README/rules/sort-type-union-intersection-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
### `sort-type-union-intersection-members`

_The `--fix` option on the command line automatically fixes problems reported by this rule._

Enforces that members of a type union/intersection are sorted alphabetically.

#### Options

You can specify the sort order using `order`.

* `"asc"` (default) - enforce ascending sort order.
* `"desc"` - enforce descending sort order.

```js
{
"rules": {
"flowtype/sort-type-union-intersection-members": [
2,
{
"order": "asc"
}
]
}
}
```

You can disable checking intersection types using `checkIntersections`.

* `true` (default) - enforce sort order of intersection members.
* `false` - do not enforce sort order of intersection members.

```js
{
"rules": {
"flowtype/sort-type-union-intersection-members": [
2,
{
"checkIntersections": true
}
]
}
}
```

You can disable checking union types using `checkUnions`.

* `true` (default) - enforce sort order of union members.
* `false` - do not enforce sort order of union members.

```js
{
"rules": {
"flowtype/sort-type-union-intersection-members": [
2,
{
"checkUnions": true
}
]
}
}
```

You can specify the ordering of groups using `groupOrder`.

Each member of the type is placed into a group, and then the rule sorts alphabetically within each group.
The ordering of groups is determined by this option.

* `keyword` - Keyword types (`any`, `string`, etc)
* `named` - Named types (`A`, `A['prop']`, `B[]`, `Array<C>`)
* `literal` - Literal types (`1`, `'b'`, `true`, etc)
* `function` - Function types (`() => void`)
* `object` - Object types (`{ a: string }`, `{ [key: string]: number }`)
* `tuple` - Tuple types (`[A, B, C]`)
* `intersection` - Intersection types (`A & B`)
* `union` - Union types (`A | B`)
* `nullish` - `null` and `undefined`

```js
{
"rules": {
"flowtype/sort-type-union-intersection-members": [
2,
{
"groupOrder": [
'keyword',
'named',
'literal',
'function',
'object',
'tuple',
'intersection',
'union',
'nullish',
]
}
]
}
}
```

<!-- assertions sortTypeUnionIntersectionMembers -->
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import requireValidFileAnnotation from './rules/requireValidFileAnnotation';
import requireVariableType from './rules/requireVariableType';
import semi from './rules/semi';
import sortKeys from './rules/sortKeys';
import sortTypeUnionIntersectionMembers from './rules/sortTypeUnionIntersectionMembers';
import spaceAfterTypeColon from './rules/spaceAfterTypeColon';
import spaceBeforeGenericBracket from './rules/spaceBeforeGenericBracket';
import spaceBeforeTypeColon from './rules/spaceBeforeTypeColon';
Expand Down Expand Up @@ -84,6 +85,7 @@ const rules = {
'require-variable-type': requireVariableType,
semi,
'sort-keys': sortKeys,
'sort-type-union-intersection-members': sortTypeUnionIntersectionMembers,
'space-after-type-colon': spaceAfterTypeColon,
'space-before-generic-bracket': spaceBeforeGenericBracket,
'space-before-type-colon': spaceBeforeTypeColon,
Expand Down Expand Up @@ -133,6 +135,7 @@ export default {
'require-variable-type': 0,
semi: 0,
'sort-keys': 0,
'sort-type-union-intersection-members': 0,
'space-after-type-colon': 0,
'space-before-generic-bracket': 0,
'space-before-type-colon': 0,
Expand Down
237 changes: 237 additions & 0 deletions src/rules/sortTypeUnionIntersectionMembers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
const groups = {
function: 'function',
intersection: 'intersection',
keyword: 'keyword',
literal: 'literal',
named: 'named',
nullish: 'nullish',
object: 'object',
tuple: 'tuple',
union: 'union',
unknown: 'unknown',
};

// eslint-disable-next-line complexity
const getGroup = (node) => {
switch (node.type) {
case 'FunctionTypeAnnotation':
return groups.function;

case 'IntersectionTypeAnnotation':
return groups.intersection;

case 'AnyTypeAnnotation':
case 'BooleanTypeAnnotation':
case 'NumberTypeAnnotation':
case 'StringTypeAnnotation':
case 'SymbolTypeAnnotation':
case 'ThisTypeAnnotation':
return groups.keyword;

case 'NullLiteralTypeAnnotation':
case 'NullableTypeAnnotation':
case 'VoidTypeAnnotation':
return groups.nullish;

case 'BooleanLiteralTypeAnnotation':
case 'NumberLiteralTypeAnnotation':
case 'StringLiteralTypeAnnotation':
return groups.literal;

case 'ArrayTypeAnnotation':
case 'IndexedAccessType':
case 'GenericTypeAnnotation':
case 'OptionalIndexedAccessType':
return groups.named;

case 'ObjectTypeAnnotation':
return groups.object;

case 'TupleTypeAnnotation':
return groups.tuple;

case 'UnionTypeAnnotation':
return groups.union;
}

return groups.unknown;
};

const fallbackSort = (a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}

return 0;
};

const sorters = {
asc: (collator, a, b) => {
return collator.compare(a, b) || fallbackSort(a, b);
},
desc: (collator, a, b) => {
return collator.compare(b, a) || fallbackSort(b, a);
},
};

const create = (context) => {
const sourceCode = context.getSourceCode();

const {
checkIntersections = true,
checkUnions = true,
groupOrder = [
groups.keyword,
groups.named,
groups.literal,
groups.function,
groups.object,
groups.tuple,
groups.intersection,
groups.union,
groups.nullish,
],
order = 'asc',
} = context.options[1] || {};

const sort = sorters[order];

const collator = new Intl.Collator('en', {
numeric: true,
sensitivity: 'base',
});

const checkSorting = (node) => {
const sourceOrder = node.types.map((type) => {
const group = groupOrder?.indexOf(getGroup(type)) ?? -1;

return {
group: group === -1 ? Number.MAX_SAFE_INTEGER : group,
node: type,
text: sourceCode.getText(type),
};
});

const expectedOrder = [...sourceOrder].sort((a, b) => {
if (a.group !== b.group) {
return a.group - b.group;
}

return sort(collator, a.text, b.text);
});

const hasComments = node.types.some((type) => {
const count =
sourceCode.getCommentsBefore(type).length +
sourceCode.getCommentsAfter(type).length;

return count > 0;
});

let prev = null;

for (let i = 0; i < expectedOrder.length; i += 1) {
const type = node.type === 'UnionTypeAnnotation' ? 'union' : 'intersection';
const current = sourceOrder[i].text;
const last = prev;

// keep track of the last token
prev = current || last;

if (!last || !current) {
continue;
}

if (expectedOrder[i].node !== sourceOrder[i].node) {
const data = {
current,
last,
order,
type,
};

const fix = (fixer) => {
const sorted = expectedOrder
.map((t) => {
return t.text;
})
.join(
node.type === 'UnionTypeAnnotation' ? ' | ' : ' & ',
);

return fixer.replaceText(node, sorted);
};

context.report({
data,
messageId: 'notSorted',
node,

// don't autofix if any of the types have leading/trailing comments
// the logic for preserving them correctly is a pain - we may implement this later
...hasComments ?
{
suggest: [
{
fix,
messageId: 'suggestFix',
},
],
} :
{fix},
});
}
}
};

return {
IntersectionTypeAnnotation (node) {
if (checkIntersections === true) {
checkSorting(node);
}
},
UnionTypeAnnotation (node) {
if (checkUnions === true) {
checkSorting(node);
}
},
};
};

export default {
create,
meta: {
fixable: 'code',
messages: {
notSorted: 'Expected {{type}} members to be in {{order}}ending order. "{{current}}" should be before "{{last}}".',
suggestFix: 'Sort members of type (removes all comments).',
},
schema: [
{
properties: {
checkIntersections: {
type: 'boolean',
},
checkUnions: {
type: 'boolean',
},
groupOrder: {
items: {
enum: Object.keys(groups),
type: 'string',
},
type: 'array',
},
order: {
enum: ['asc', 'desc'],
type: 'string',
},
},
type: 'object',
},
],
},
};
Loading

0 comments on commit fa4207d

Please sign in to comment.