Skip to content

Commit

Permalink
Fix autofix for options and multiOptions sorting rules (ivov#120)
Browse files Browse the repository at this point in the history
* Fix autofix for options and multiOptions sorting rules

* Move `prettier` to regular deps
  • Loading branch information
ivov authored Sep 8, 2022
1 parent 4bc8ad1 commit 945e87f
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 30 deletions.
22 changes: 22 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
40 changes: 31 additions & 9 deletions lib/rules/node-param-multi-options-type-unsorted-items.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import prettier from "prettier";
import {
MIN_ITEMS_TO_ALPHABETIZE,
MIN_ITEMS_TO_ALPHABETIZE_SPELLED_OUT,
} from "../constants";
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),
Expand All @@ -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),
});
}
},
Expand Down
48 changes: 38 additions & 10 deletions lib/rules/node-param-options-type-unsorted-items.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import prettier from "prettier";
import {
MIN_ITEMS_TO_ALPHABETIZE,
MIN_ITEMS_TO_ALPHABETIZE_SPELLED_OUT,
} from "../constants";
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),
Expand All @@ -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;
}
}
10 changes: 4 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
Expand Down
10 changes: 6 additions & 4 deletions tests/node-param-multi-options-type-unsorted-items.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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.',
},
],
};`,
Expand Down

0 comments on commit 945e87f

Please sign in to comment.