From 945e87f96e6303421d0d4b0e8ca265f23e179ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 8 Sep 2022 13:21:14 +0200 Subject: [PATCH] Fix autofix for `options` and `multiOptions` sorting rules (#120) * Fix autofix for options and multiOptions sorting rules * Move `prettier` to regular deps --- lib/constants.ts | 22 +++++++++ ...param-multi-options-type-unsorted-items.ts | 40 ++++++++++++---- .../node-param-options-type-unsorted-items.ts | 48 +++++++++++++++---- package-lock.json | 10 ++-- package.json | 2 +- ...-multi-options-type-unsorted-items.test.ts | 10 ++-- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/lib/constants.ts b/lib/constants.ts index c38d375..7ba1905 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -195,3 +195,25 @@ export const CREDS_EXEMPTED_FROM_API_SUFFIX = [ "SshPrivateKey", "TimescaleDb", ]; + +// ---------------------------------- +// formatting +// ---------------------------------- + +/** + * From: https://raw.githubusercontent.com/n8n-io/n8n/master/.prettierrc.js + */ + export const PRETTIER_CONFIG = { + semi: true, + trailingComma: 'all', + bracketSpacing: true, + useTabs: true, + tabWidth: 2, + arrowParens: 'always', + singleQuote: true, + quoteProps: 'as-needed', + endOfLine: 'lf', + printWidth: 100, + + parser: 'babel' // to silence warning, not part of n8n's config +} as const; \ No newline at end of file diff --git a/lib/rules/node-param-multi-options-type-unsorted-items.ts b/lib/rules/node-param-multi-options-type-unsorted-items.ts index f71754f..ef9761c 100644 --- a/lib/rules/node-param-multi-options-type-unsorted-items.ts +++ b/lib/rules/node-param-multi-options-type-unsorted-items.ts @@ -1,3 +1,4 @@ +import prettier from "prettier"; import { MIN_ITEMS_TO_ALPHABETIZE, MIN_ITEMS_TO_ALPHABETIZE_SPELLED_OUT, @@ -5,6 +6,8 @@ import { import { utils } from "../ast/utils"; import { id } from "../ast/identifiers"; import { getters } from "../ast/getters"; +import { PRETTIER_CONFIG } from "../constants"; +import { toOptions } from "./node-param-options-type-unsorted-items"; export default utils.createRule({ name: utils.getRuleName(module), @@ -29,26 +32,45 @@ export default utils.createRule({ if (!id.nodeParam.isMultiOptionsType(node)) return; - const options = getters.nodeParam.getOptions(node); + const optionsNode = getters.nodeParam.getOptions(node); - if (!options) return; + if (!optionsNode) return; + + if (optionsNode.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; - if (options.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; + if (/^\d+$/.test(optionsNode.value[0].value)) return; // do not sort numeric strings - const sortedOptions = [...options.value].sort(utils.optionComparator); + const optionsSource = context + .getSourceCode() + .getText(optionsNode.ast.value); - if (!utils.areIdenticallySortedOptions(options.value, sortedOptions)) { - const baseIndentation = utils.getBaseIndentationForOption(options); + const options = toOptions(optionsSource); + + if (!options) return; - const fixed = utils.formatItems(sortedOptions, baseIndentation); + const sortedOptions = [...options].sort(utils.optionComparator); + if (!utils.areIdenticallySortedOptions(options, sortedOptions)) { const displayOrder = utils.toDisplayOrder(sortedOptions); + const sortedOptionsSource = JSON.stringify(sortedOptions, null, 2); + + const unformattedNewSource = context + .getSourceCode() + .getText() + .replace(optionsSource, sortedOptionsSource); + + const formattedNewSource = prettier + .format(unformattedNewSource, PRETTIER_CONFIG) + .trim(); // consume Prettier's EoF newline + + const fullAst = context.getSourceCode().ast; + context.report({ messageId: "sortItems", - node: options.ast, + node: optionsNode.ast, data: { displayOrder }, - fix: (fixer) => fixer.replaceText(options.ast, `options: ${fixed}`), + fix: (fixer) => fixer.replaceText(fullAst, formattedNewSource), }); } }, diff --git a/lib/rules/node-param-options-type-unsorted-items.ts b/lib/rules/node-param-options-type-unsorted-items.ts index 6906af9..b3832f9 100644 --- a/lib/rules/node-param-options-type-unsorted-items.ts +++ b/lib/rules/node-param-options-type-unsorted-items.ts @@ -1,3 +1,4 @@ +import prettier from "prettier"; import { MIN_ITEMS_TO_ALPHABETIZE, MIN_ITEMS_TO_ALPHABETIZE_SPELLED_OUT, @@ -5,6 +6,7 @@ import { import { utils } from "../ast/utils"; import { id } from "../ast/identifiers"; import { getters } from "../ast/getters"; +import { PRETTIER_CONFIG } from "../constants"; export default utils.createRule({ name: utils.getRuleName(module), @@ -29,31 +31,57 @@ export default utils.createRule({ if (!id.nodeParam.isOptionsType(node)) return; - const options = getters.nodeParam.getOptions(node); + const optionsNode = getters.nodeParam.getOptions(node); - if (!options) return; + if (!optionsNode) return; + + if (optionsNode.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; - if (options.value.length < MIN_ITEMS_TO_ALPHABETIZE) return; + if (/^\d+$/.test(optionsNode.value[0].value)) return; // do not sort numeric strings - if (/^\d+$/.test(options.value[0].value)) return; // do not sort numeric strings + const optionsSource = context + .getSourceCode() + .getText(optionsNode.ast.value); - const sortedOptions = [...options.value].sort(utils.optionComparator); + const options = toOptions(optionsSource); - if (!utils.areIdenticallySortedOptions(options.value, sortedOptions)) { - const baseIndentation = utils.getBaseIndentationForOption(options); + if (!options) return; - const fixed = utils.formatItems(sortedOptions, baseIndentation); + const sortedOptions = [...options].sort(utils.optionComparator); + if (!utils.areIdenticallySortedOptions(options, sortedOptions)) { const displayOrder = utils.toDisplayOrder(sortedOptions); + const sortedOptionsSource = JSON.stringify(sortedOptions, null, 2); + + const unformattedNewSource = context + .getSourceCode() + .getText() + .replace(optionsSource, sortedOptionsSource); + + const formattedNewSource = prettier + .format(unformattedNewSource, PRETTIER_CONFIG) + .trim(); // consume Prettier's EoF newline + + const fullAst = context.getSourceCode().ast; + context.report({ messageId: "sortItems", - node: options.ast, + node: optionsNode.ast, data: { displayOrder }, - fix: (fixer) => fixer.replaceText(options.ast, `options: ${fixed}`), + fix: (fixer) => fixer.replaceText(fullAst, formattedNewSource), }); } }, }; }, }); + +export function toOptions(optionsSource: string): Array<{ name: string }> | null { + try { + return eval(`(${optionsSource})`); + } catch (error) { + console.error("Failed to eval options source", optionsSource, error); + return null; + } +} diff --git a/package-lock.json b/package-lock.json index 8d963cd..acd4d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-n8n-nodes-base", - "version": "1.7.0", + "version": "1.8.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-n8n-nodes-base", - "version": "1.7.0", + "version": "1.8.1", "license": "MIT", "dependencies": { "@typescript-eslint/utils": "^5.17.0", @@ -15,6 +15,7 @@ "indefinite": "^2.4.1", "pascal-case": "^3.1.2", "pluralize": "^8.0.0", + "prettier": "^2.7.1", "sentence-case": "^3.0.4", "title-case": "^3.0.3" }, @@ -34,7 +35,6 @@ "eslint-plugin-prettier": "^4.2.1", "jest": "^28.1.3", "outdent": "^0.8.0", - "prettier": "^2.7.1", "shelljs": "^0.8.5", "tiny-glob": "^0.2.9", "typescript": "^4.6.2" @@ -6224,7 +6224,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12418,8 +12417,7 @@ "prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==" }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/package.json b/package.json index 69a9c82..3371ffb 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "indefinite": "^2.4.1", "pascal-case": "^3.1.2", "pluralize": "^8.0.0", + "prettier": "^2.7.1", "sentence-case": "^3.0.4", "title-case": "^3.0.3" }, @@ -54,7 +55,6 @@ "eslint-plugin-prettier": "^4.2.1", "jest": "^28.1.3", "outdent": "^0.8.0", - "prettier": "^2.7.1", "shelljs": "^0.8.5", "tiny-glob": "^0.2.9", "typescript": "^4.6.2" diff --git a/tests/node-param-multi-options-type-unsorted-items.test.ts b/tests/node-param-multi-options-type-unsorted-items.test.ts index 38735de..05f8a88 100644 --- a/tests/node-param-multi-options-type-unsorted-items.test.ts +++ b/tests/node-param-multi-options-type-unsorted-items.test.ts @@ -168,7 +168,7 @@ ruleTester().run(getRuleName(module), rule, { { name: 'Date Equal', value: 'date_equal', - description: 'Field is date. Format: \\'YYYY-MM-DD\\'', + description: "Field is date. Format: 'YYYY-MM-DD'", }, { name: 'Invoice Deleted', @@ -178,12 +178,13 @@ ruleTester().run(getRuleName(module), rule, { { name: 'Invoice Generated', value: 'invoice_generated', - description: 'Event triggered when a new invoice is generated. In case of metered billing, this event is triggered when a \\'Pending\\' invoice is closed.', + description: + 'Event triggered when a new invoice is generated. In case of metered billing, this event is triggered when a "Pending" invoice is closed.', }, { name: 'Subscription Renewal Reminder', value: 'subscription_renewal_reminder', - description: 'Triggered 3 days before each subscription\\'s renewal.', + description: "Triggered 3 days before each subscription's renewal.", }, { name: 'Transaction Created', @@ -198,7 +199,8 @@ ruleTester().run(getRuleName(module), rule, { { name: 'Transaction Updated', value: 'transaction_updated', - description: 'Triggered when a transaction is updated. E.g. (1) When a transaction is removed, (2) or when an excess payment is applied on an invoice, (3) or when amount_capturable gets updated.', + description: + 'Triggered when a transaction is updated. E.g. (1) When a transaction is removed, (2) or when an excess payment is applied on an invoice, (3) or when amount_capturable gets updated.', }, ], };`,