From e3e621d5aeda67931217cab874f16ebdf623d8ec Mon Sep 17 00:00:00 2001 From: Ryan Mark Date: Tue, 12 May 2020 07:08:22 -0700 Subject: [PATCH] feat: support printWidth and optionally inherit from prettier config --- lib/rules/decorator-position.js | 158 +++++++++++++----- package.json | 6 +- scripts/smoke-test.sh | 8 + .../-rules/position-prettier/.eslintrc.js | 10 ++ .../-rules/position-prettier/defaults.js | 11 ++ tests/lib/rules/decorator-position.js | 3 + 6 files changed, 155 insertions(+), 41 deletions(-) diff --git a/lib/rules/decorator-position.js b/lib/rules/decorator-position.js index 4b1d74d5..ae9da30c 100644 --- a/lib/rules/decorator-position.js +++ b/lib/rules/decorator-position.js @@ -1,5 +1,9 @@ 'use strict'; +const { CLIEngine } = require('eslint'); + +const cli = new CLIEngine(); + module.exports = { meta: { type: 'layout', @@ -50,6 +54,9 @@ module.exports = { type: 'object', additionalProperties: false, properties: { + printWidth: { + type: 'number', + }, properties: { $ref: '#/definitions/alignmentOptions' }, methods: { $ref: '#/definitions/alignmentOptions' }, @@ -109,6 +116,7 @@ const METHODS = 'methods'; // specifics set by the eslint config const defaultOptions = { + printWidth: 100, [PROPERTIES]: PREFER_INLINE, [METHODS]: ABOVE, overrides: { @@ -117,9 +125,17 @@ const defaultOptions = { }, }; +let prettier; + function decoratorPositionRule(context) { + if (prettier && prettier.clearConfigCache) { + prettier.clearConfigCache(); + } + const userOptions = context.options[0] || {}; - const options = normalizeOptions(userOptions); + const filePath = context.getFilename(); + const prettierOptions = lineLength(userOptions, filePath); + const options = normalizeOptions(prettierOptions); return { 'ClassProperty[decorators.length=1]:exit'(node) { @@ -162,16 +178,21 @@ function positionDecorator(context, node, options) { (key === PROPERTIES && node.type === 'ClassProperty') || (key === METHODS && node.type === 'MethodDefinition'); + let overridesConfig; if (isMemberRelevant) { if (position === PREFER_INLINE) { - placeDecoratorsBesideProperty(context, node, { + overridesConfig = { + printWidth: options.printWidth, overrides: { [PREFER_INLINE]: decorators }, - }); + }; + placeDecoratorsBesideProperty(context, node, overridesConfig); } else { // above - placeDecoratorsAboveProperty(context, node, { + overridesConfig = { + printWidth: options.printWidth, overrides: { [ABOVE]: decorators }, - }); + }; + placeDecoratorsAboveProperty(context, node, overridesConfig); } } }); @@ -185,25 +206,37 @@ function placeDecoratorsBesideProperty(context, node, options) { const config = normalizeConfig(decoratorConfig, INTENT.SAME_LINE); const info = decoratorInfo(node, config); - if (!info.needsTransform) { - continue; - } + const decorator = node.decorators[0]; + const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); - context.report({ - node, - message: `Expected @${info.name} to be inline.`, + const whitespaceStart = decorator.range[1]; + const whitespaceEnd = token.range[0]; - fix(fixer) { - const decorator = node.decorators[0]; - const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); - const whitespaceStart = decorator.range[1]; - const whitespaceEnd = token.range[0]; + const whitespaceLength = whitespaceEnd - whitespaceStart; - // delete the whitespace between the end of the decorator - // and the decorated thing - return fixer.replaceTextRange([whitespaceStart, whitespaceEnd], ' '); - }, - }); + const totalLineLength = calculateTotalLineLength(context, node, token) + whitespaceLength; + const lessThanOrEqualToPrintWidth = totalLineLength <= Number(options.printWidth); + + if (lessThanOrEqualToPrintWidth) { + if (!info.needsTransform) { + continue; + } + + context.report({ + node, + message: `Expected @${info.name} to be inline.`, + + fix(fixer) { + const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); + const whitespaceStart = decorator.range[1]; + const whitespaceEnd = token.range[0]; + + // delete the whitespace between the end of the decorator + // and the decorated thing + return fixer.replaceTextRange([whitespaceStart, whitespaceEnd], ' '); + }, + }); + } } } @@ -215,29 +248,41 @@ function placeDecoratorsAboveProperty(context, node, options) { const config = normalizeConfig(decoratorConfig, INTENT.DIFFERENT_LINES); const info = decoratorInfo(node, config); - if (!info.needsTransform) { - continue; - } + const decorator = node.decorators[0]; + const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); - context.report({ - node, - message: `Expected @${info.name} to be on the line above.`, + const whitespaceStart = decorator.range[1]; + const whitespaceEnd = token.range[0] - 1; - fix(fixer) { - const decorator = node.decorators[0]; - const indentation = decorator.loc.start.column - 1; - const padding = indentation > 0 ? ' '.repeat(indentation) : ''; - const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); + const whitespaceLength = whitespaceEnd - whitespaceStart; + // eslint-disable-next-line no-unused-vars + const totalLineLength = calculateTotalLineLength(context, node, token) + whitespaceLength; + const greaterThanOrEqualToPrintWidth = true; - const whitespaceStart = decorator.range[1]; - const whitespaceEnd = token.range[0] - 1; + if (greaterThanOrEqualToPrintWidth) { + if (!info.needsTransform) { + continue; + } - // delete the space(s) between the decorator and the - // decorated thing with a newline, matching the - // indentation of the decorator - return fixer.replaceTextRange([whitespaceStart, whitespaceEnd], `\n${padding}`); - }, - }); + context.report({ + node, + message: `Expected @${info.name} to be on the line above.`, + + fix(fixer) { + const indentation = decorator.loc.start.column - 1; + const padding = indentation > 0 ? ' '.repeat(indentation) : ''; + const token = context.getSourceCode().getTokenAfter(decorator, { includeComments: true }); + + const whitespaceStart = decorator.range[1]; + const whitespaceEnd = token.range[0] - 1; + + // delete the space(s) between the decorator and the + // decorated thing with a newline, matching the + // indentation of the decorator + return fixer.replaceTextRange([whitespaceStart, whitespaceEnd], `\n${padding}`); + }, + }); + } } } @@ -245,6 +290,17 @@ function placeDecoratorsAboveProperty(context, node, options) { // Helpers // /////////////////////////////////// +function calculateTotalLineLength(context, node, token) { + const decorator = node.decorators[0]; + const punctuator = context.getSourceCode().getTokenAfter(token, { includeComments: true }); + + const [decStart, decEnd] = decorator.range; + const [tokenStart, tokenEnd] = token.range; + const [puncStart, puncEnd] = punctuator.range; + + return decEnd - decStart + tokenEnd - tokenStart + puncEnd - puncStart; +} + function normalizeOptions(userOptions) { const options = Object.assign({}, defaultOptions, userOptions); @@ -265,6 +321,28 @@ function configuredDecoratorsInOptions(options) { return allConfigs.map((config) => config[0]); } +function lineLength(userOptions, filePath) { + if (!prettier) { + prettier = require('prettier'); + } + + const eslintPrettierRules = cli.getConfigForFile(filePath).rules['prettier/prettier'] || []; + const eslintPrettierOptions = eslintPrettierRules[1] || {}; + const usePrettierrc = !eslintPrettierOptions || eslintPrettierOptions.usePrettierrc !== false; + const prettierRcOptions = usePrettierrc + ? prettier.resolveConfig.sync(filePath, { + editorconfig: true, + }) + : {}; + + const prettierOptions = Object.assign({}, prettierRcOptions, eslintPrettierOptions, { + filePath, + }); + const r = Object.assign({}, prettierOptions, userOptions); + // console.log(r); + return r; +} + function normalizeConfig(config, intent) { let name; let options = {}; diff --git a/package.json b/package.json index 65833c49..0f04016b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "lint:js": "eslint . --cache", "start": "yarn run test:watch", "test": "jest", + "test:debug": "node --inspect node_modules/.bin/jest --watch --runInBand", + "test:debug-watch": "node --inspect node_modules/.bin/jest --runInBand", "test:coverage": "jest --coverage", "test:watch": "jest --watchAll", "update": "node ./scripts/update-rules.js", @@ -84,7 +86,9 @@ "dependencies": { "@ember-data/rfc395-data": "^0.0.4", "ember-rfc176-data": "^0.3.12", - "snake-case": "^3.0.3" + "snake-case": "^3.0.3", + "prettier": "2.0.5", + "eslint": "7.2.0" }, "changelog": { "repo": "NullVoxPopuli/eslint-plugin-decorator-position", diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 40dab25a..b9c53c51 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -3,6 +3,14 @@ set -e target=$1 +echo "About to test scenario: $target" +echo "" +echo "Available scenarios:" +echo " -rules/position-default" +echo " -rules/position-prettier" +echo " ember" +echo "" + name="eslint-plugin-decorator-position" yarn diff --git a/smoke-tests/-rules/position-prettier/.eslintrc.js b/smoke-tests/-rules/position-prettier/.eslintrc.js index a0dd4e37..03c11c89 100644 --- a/smoke-tests/-rules/position-prettier/.eslintrc.js +++ b/smoke-tests/-rules/position-prettier/.eslintrc.js @@ -3,5 +3,15 @@ module.exports = { plugins: ['prettier'], rules: { 'decorator-position/decorator-position': ['error'], + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + printWidth: 100, + semi: true, + trailingComma: 'es5', + quoteProps: 'preserve', + }, + ], }, }; diff --git a/smoke-tests/-rules/position-prettier/defaults.js b/smoke-tests/-rules/position-prettier/defaults.js index 0725a069..060ec866 100644 --- a/smoke-tests/-rules/position-prettier/defaults.js +++ b/smoke-tests/-rules/position-prettier/defaults.js @@ -21,4 +21,15 @@ export class Foo { @foo async myMethod3() {} + + @alias( + 'foo.bar.baz.someReallyLongPropertyNameThatIsTooLongToBeInlineOrItBreaksPrettier.Prettier.is.Set.to.100' + ) + myProp; + + @hasMany('some-very-long-relationship-name-channel-context-chain', { + async: true, + inverse: 'members', + }) + memberOf; } diff --git a/tests/lib/rules/decorator-position.js b/tests/lib/rules/decorator-position.js index 76df1081..dd914e56 100644 --- a/tests/lib/rules/decorator-position.js +++ b/tests/lib/rules/decorator-position.js @@ -23,6 +23,9 @@ ruleTester.run('JS: decorator-position', rule, { stripIndent` class Foo { @foo foo; + + @alias('foo.bar.baz.someReallyLongPropertyNameThatIsTooLongToBeInlineOrItBreaksPrettier.Prettier.is.Set.to.100') + myProp; } `, stripIndent`