Skip to content

Commit

Permalink
Add an unique alphabetical order array rule
Browse files Browse the repository at this point in the history
  • Loading branch information
pashak09 committed Jun 16, 2023
1 parent 5a18663 commit de2d444
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 112 deletions.
3 changes: 1 addition & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@
"curly": "error",
"no-unreachable": "error",
"no-console": "error",
"@typescript-eslint/no-unnecessary-condition": ["warn"],
"import/no-relative-parent-imports": "error"
"@typescript-eslint/no-unnecessary-condition": ["warn"]
},
"parserOptions": {
"ecmaVersion": 2021,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.yarn/*
node_modules
dist
/.tsbuildinfo
34 changes: 7 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# eslint-plugin-github
# eslint-plugin-array-tokens-sorter

## Purpose

Expand All @@ -17,6 +17,9 @@ const SCOPE: unknown[] = [];
export default [...SCOPE, A, B, C];

export default [...SCOPE, A, B, C]; // eslint: enable-alphabetical-order-array - a second sample of usage

// eslint: enable-unique-alphabetical-order-array
export default [...SCOPE, A, A, B, C];
```

After fixing the code
Expand All @@ -32,6 +35,9 @@ const SCOPE: unknown[] = [];
export default [...SCOPE, A, B, C];

export default [...SCOPE, A, B, C]; // eslint: enable-alphabetical-order-array - a second sample of usage

// eslint: enable-unique-alphabetical-order-array
export default [...SCOPE, A, B, C];
```

## Installation
Expand All @@ -54,29 +60,3 @@ JSON ESLint config example:
}
}
```

If you want to indicate the files for research you can use:

```json
{
"overrides": [
{
"files": [
"src/**/*/index.ts",
"src/entities.ts"
],
"plugins": [
"array-tokens-sorter"
],
"rules": {
"array-tokens-sorter/array": [
"error",
{
"spreadVariablesFirst": true
}
]
}
}
]
}
```
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-array-tokens-sorter",
"version": "0.0.4",
"version": "0.0.5",
"description": "ESLint rule to enforce alphabetical sorting of array elements",
"keywords": [
"ESLint",
Expand All @@ -27,7 +27,6 @@
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.7",
"rimraf": "^4.4.1",
Expand Down
4 changes: 4 additions & 0 deletions src/enums/Rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Rules {
ALPHABETICAL_ORDER = 'eslint: enable-alphabetical-order-array',
UNIQUE_ALPHABETICAL_ORDER = 'eslint: enable-unique-alphabetical-order-array',
}
110 changes: 30 additions & 80 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,15 @@
import { Rule, SourceCode } from 'eslint';
import { Rule } from 'eslint';
import { ArrayExpression, Comment } from 'estree';

interface Options {
readonly spreadVariablesFirst: boolean;
}

interface NodeItem {
readonly value: string;
readonly isSpread: boolean;
}

const RULE_INDICATOR = 'eslint: enable-alphabetical-order-array';

function sortArrayItems(elements: NodeItem[], spreadVariablesFirst: boolean): readonly NodeItem[] {
return elements.sort((a: NodeItem, b: NodeItem): number => {
if (spreadVariablesFirst === true) {
if (a.isSpread && !b.isSpread) {
return -1;
}

if (!a.isSpread && b.isSpread) {
return 1;
}
}

return a.value.localeCompare(b.value);
});
}

function prepareArrayItems(node: ArrayExpression, sourceCode: SourceCode): readonly NodeItem[] {
const elements: NodeItem[] = new Array(node.elements.length);
let cursor = 0;

for (const element of node.elements) {
if (element === null) {
continue;
}

if (element.type === 'SpreadElement') {
elements[cursor] = {
value: sourceCode.getText(element.argument),
isSpread: true,
};
} else {
elements[cursor] = {
value: sourceCode.getText(element),
isSpread: false,
};
}

cursor++;
}
import { Rules } from './enums/Rules';
import { arrayTokesRules } from './rules';

return elements;
}

function checkNode(node: ArrayExpression, sourceCode: SourceCode, options: Options, context: Rule.RuleContext): void {
const elements = prepareArrayItems(node, sourceCode);
const sortedElements = sortArrayItems([...elements], options.spreadVariablesFirst || false);

if (sortedElements.some((element: NodeItem, index: number): boolean => element !== elements[index])) {
const newCode = `[${sortedElements
.map((element: NodeItem): string => (element.isSpread ? `...${element.value}` : element.value))
.join(', ')}]`;
export type Options = {
readonly spreadVariablesFirst: boolean;
};

context.report({
node,
message: 'Array elements are not sorted alphabetically.',
fix: (fixer: Rule.RuleFixer): Rule.Fix => fixer.replaceText(node, newCode),
});
}
function hasESLintRule(value: string): value is Rules {
return value === Rules.UNIQUE_ALPHABETICAL_ORDER || value === Rules.ALPHABETICAL_ORDER;
}

const array: Rule.RuleModule = {
Expand All @@ -95,35 +35,45 @@ const array: Rule.RuleModule = {
},
create(context: Rule.RuleContext): Rule.RuleListener {
const sourceCode = context.getSourceCode();
const options = context.options[0] || {};
const lineMapper = new Set<number>();
const options: Options = context.options[0] || {};
const lineMapper = new Map<number, Rules>();

return {
Program(): void {
const comments = sourceCode.getAllComments();

comments.forEach((comment: Comment): void => {
if (comment.value.trim() === RULE_INDICATOR) {
const linePosition = comment.loc?.start;
const line = comment.value.trim();

if (!hasESLintRule(line)) {
return;
}

if (linePosition === undefined) {
return;
}
const linePosition = comment.loc?.start;

//added a current line for the case [B, A, B, C] // enable-alphabetical-order-array
lineMapper.add(linePosition.line);
lineMapper.add(linePosition.line + 1);
if (linePosition === undefined) {
return;
}

//added a current line for the case [B, A, B, C] // enable-alphabetical-order-array
lineMapper.set(linePosition.line, line);
lineMapper.set(linePosition.line + 1, line);
});
},
ArrayExpression(node: ArrayExpression): void {
const position = node.loc?.start;

if (position === undefined || lineMapper.has(position.line) === false) {
if (position === undefined) {
return;
}

const esRule = lineMapper.get(position.line);

if (esRule === undefined) {
return;
}

checkNode(node, sourceCode, options, context);
arrayTokesRules(node, context, esRule, options);
},
};
},
Expand Down
29 changes: 29 additions & 0 deletions src/rules/alphabeticalOrderArrayRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Rule } from 'eslint';
import { ArrayExpression } from 'estree';

import { Options } from '../index';
import { arrayFixer } from '../utils/arrayFixer';
import { arrayItemsScanner, NodeItem } from '../utils/arrayItemsScanner';
import { arrayItemsSorter } from '../utils/arrayItemsSorter';

export function alphabeticalOrderErrorsFixer(
elements: readonly NodeItem[],
sortedElements: readonly NodeItem[],
node: ArrayExpression,
context: Rule.RuleContext,
): boolean {
if (sortedElements.some((element: NodeItem, index: number): boolean => element !== elements[index])) {
arrayFixer(sortedElements, 'Array elements are not sorted alphabetically.', node, context);

return true;
}

return false;
}

export function alphabeticalOrderArrayRule(node: ArrayExpression, context: Rule.RuleContext, options: Options): void {
const elements = arrayItemsScanner(node, context.getSourceCode());
const sortedElements = arrayItemsSorter([...elements], options.spreadVariablesFirst || false);

alphabeticalOrderErrorsFixer(elements, sortedElements, node, context);
}
19 changes: 19 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Rule } from 'eslint';
import { ArrayExpression } from 'estree';

import { Rules } from '../enums/Rules';
import { Options } from '../index';

import { alphabeticalOrderArrayRule } from './alphabeticalOrderArrayRule';
import { uniqueAlphabeticalOrderArrayRule } from './uniqueAlphabeticalOrderArrayRule';

type Callback = (node: ArrayExpression, context: Rule.RuleContext, options: Options) => void;

const ruleCallbackMapper: Record<Rules, Callback> = {
[Rules.ALPHABETICAL_ORDER]: alphabeticalOrderArrayRule,
[Rules.UNIQUE_ALPHABETICAL_ORDER]: uniqueAlphabeticalOrderArrayRule,
};

export function arrayTokesRules(node: ArrayExpression, context: Rule.RuleContext, rule: Rules, options: Options): void {
ruleCallbackMapper[rule](node, context, options);
}
52 changes: 52 additions & 0 deletions src/rules/uniqueAlphabeticalOrderArrayRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Rule } from 'eslint';
import { ArrayExpression } from 'estree';

import { Options } from '../index';
import { arrayFixer } from '../utils/arrayFixer';
import { arrayItemsScanner, NodeItem } from '../utils/arrayItemsScanner';
import { arrayItemsSorter } from '../utils/arrayItemsSorter';

import { alphabeticalOrderErrorsFixer } from './alphabeticalOrderArrayRule';

export function uniqueAlphabeticalOrderArrayCheckerFixer(
sortedElements: readonly NodeItem[],
node: ArrayExpression,
context: Rule.RuleContext,
): void {
const uniqueItemSet = new Set<string>();
const uniqueItems: NodeItem[] = new Array(sortedElements.length);
let cursor = 0;
let hasUniqueItems = true;

for (const element of sortedElements) {
if (uniqueItemSet.has(element.value) === false) {
uniqueItems[cursor] = element;
cursor++;
uniqueItemSet.add(element.value);

continue;
}

hasUniqueItems = false;
}

if (hasUniqueItems !== false) {
return;
}

arrayFixer(uniqueItems, 'Array elements are not uniqueness.', node, context);
}

export function uniqueAlphabeticalOrderArrayRule(
node: ArrayExpression,
context: Rule.RuleContext,
options: Options,
): void {
const elements = arrayItemsScanner(node, context.getSourceCode());
const sortedElements = arrayItemsSorter([...elements], options.spreadVariablesFirst || false);

//has no alphabetical errors, validate uniqueness
if (alphabeticalOrderErrorsFixer(elements, sortedElements, node, context) === false) {
uniqueAlphabeticalOrderArrayCheckerFixer(sortedElements, node, context);
}
}
21 changes: 21 additions & 0 deletions src/utils/arrayFixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Rule } from 'eslint';
import { ArrayExpression } from 'estree';

import { NodeItem } from './arrayItemsScanner';

export function arrayFixer(
sortedElements: readonly NodeItem[],
message: string,
node: ArrayExpression,
context: Rule.RuleContext,
): void {
const newCode = `[${sortedElements
.map((element: NodeItem): string => (element.isSpread ? `...${element.value}` : element.value))
.join(', ')}]`;

context.report({
node,
message,
fix: (fixer: Rule.RuleFixer): Rule.Fix => fixer.replaceText(node, newCode),
});
}
34 changes: 34 additions & 0 deletions src/utils/arrayItemsScanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SourceCode } from 'eslint';
import { ArrayExpression } from 'estree';

export type NodeItem = {
readonly value: string;
readonly isSpread: boolean;
};

export function arrayItemsScanner(node: ArrayExpression, sourceCode: SourceCode): readonly NodeItem[] {
const elements: NodeItem[] = new Array(node.elements.length);
let cursor = 0;

for (const element of node.elements) {
if (element === null) {
continue;
}

if (element.type === 'SpreadElement') {
elements[cursor] = {
value: sourceCode.getText(element.argument),
isSpread: true,
};
} else {
elements[cursor] = {
value: sourceCode.getText(element),
isSpread: false,
};
}

cursor++;
}

return elements;
}
Loading

0 comments on commit de2d444

Please sign in to comment.