diff --git a/projects/eslint-plugin-experience/configs/taiga.js b/projects/eslint-plugin-experience/configs/taiga.js index 4ce609b0..cb8f60f8 100644 --- a/projects/eslint-plugin-experience/configs/taiga.js +++ b/projects/eslint-plugin-experience/configs/taiga.js @@ -23,6 +23,54 @@ module.exports = { ], '@taiga-ui/experience/strict-tui-doc-example': 'error', '@taiga-ui/experience/no-assert-without-ng-dev-mode': 'error', + '@taiga-ui/experience/decorator-key-sort': [ + 'error', + { + decorators: { + Component: [ + 'moduleId', + 'standalone', + 'signal', + 'selector', + 'template', + 'templateUrl', + 'styleUrls', + 'styles', + 'encapsulation', + 'changeDetection', + 'providers', + 'viewProviders', + 'animations', + 'entryComponents', + 'preserveWhitespaces', + 'interpolation', + ], + NgModule: [ + 'id', + 'jit', + 'imports', + 'declarations', + 'providers', + 'exports', + 'entryComponents', + 'bootstrap', + 'schemas', + ], + Directive: [ + 'selector', + 'inputs', + 'outputs', + 'providers', + 'exportAs', + 'queries', + 'host', + 'jit', + ], + Injectable: ['providedIn'], + Pipe: ['name', 'pure'], + }, + }, + ], }, }, ], diff --git a/projects/eslint-plugin-experience/index.js b/projects/eslint-plugin-experience/index.js index 57f14120..db38e215 100644 --- a/projects/eslint-plugin-experience/index.js +++ b/projects/eslint-plugin-experience/index.js @@ -40,5 +40,6 @@ module.exports = { 'no-private-esnext-fields': require('./rules/no-private-esnext-fields'), 'strict-tui-doc-example': require('./rules/strict-tui-doc-example'), 'no-assert-without-ng-dev-mode': require('./rules/no-assert-without-ng-dev-mode'), + 'decorator-key-sort': require('./rules/decorator-key-sort'), }, }; diff --git a/projects/eslint-plugin-experience/rules/decorator-key-sort.js b/projects/eslint-plugin-experience/rules/decorator-key-sort.js new file mode 100644 index 00000000..ebaf486d --- /dev/null +++ b/projects/eslint-plugin-experience/rules/decorator-key-sort.js @@ -0,0 +1,75 @@ +const DEFAULT_OPTIONS = { + decorators: {}, +}; + +/** + * @type {import(`eslint`).Rule.RuleModule} + */ +module.exports = { + meta: { + type: 'problem', + fixable: 'code', + schema: [ + { + type: `object`, + properties: { + decorators: { + type: `object`, + description: `Decorators names and his keys order`, + properties: { + additionalProperties: true, + }, + }, + }, + additionalProperties: false, + }, + ], + }, + create(context) { + const {decorators: ORDER} = { + ...DEFAULT_OPTIONS, + ...(context.options[0] || {}), + }; + + return { + ClassDeclaration(node) { + const decorators = Array.from(node.decorators ?? []); + + for (const decorator of decorators) { + const expression = decorator.expression; + const decoratorName = expression.callee.name; + + if (decoratorName in (ORDER || {})) { + const orderList = ORDER[decoratorName]; + const arguments = Array.from(expression.arguments ?? []); + + for (const argument of arguments) { + const properties = Array.from(argument.properties ?? []); + const current = properties.map(prop => prop.key.name); + const correct = getCorrectOrderRelative(orderList, current); + + if (!isCorrectSortedAccording(correct, current)) { + context.report({ + node: expression, + message: `Incorrect order keys in @${decoratorName} decorator, please sort by [${correct.join( + ' -> ', + )}]`, + }); + } + } + } + } + }, + }; + }, +}; + +function isCorrectSortedAccording(correctOrder, currentOrder) { + let excludeUnknown = currentOrder.filter(item => correctOrder.includes(item)); + + return JSON.stringify(correctOrder) === JSON.stringify(excludeUnknown); +} + +function getCorrectOrderRelative(correctOrder, currentOrder) { + return correctOrder.filter(item => currentOrder.includes(item)); +}