From 9b0023a4996ecdd7dfcb30abd1678091a78f3064 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 30 Mar 2020 11:42:21 -0700 Subject: [PATCH] feat(eslint-plugin-internal): add plugin-test-formatting rule (#1821) The strings that are used for eslint plugins will not be checked for formatting. This can lead to diff noise as one contributor adjusts formatting, uses different quotes, etc. This rule just enforces the following: - all code samples are formatted with prettier - all single line tests do not use backticks - all multiline tests have: - no code on the first line - no code on the last line - the closing backtick indentation === property indentation - one of the following indentations: - no indentation at all - indentation of 1 + object indent examples of enforced style: ```ts ruleTester.run('foo', rule, { valid: [ 'const a = 1;', ` const a = 1; `, ` const a = 1; `, { code: 'const a = 1;', }, { code: ` const a = 1; `, }, { code: ` const a = 1; `, }, ], invalid: [ { code: 'const a = 1;', }, { code: ` const a = 1; `, }, { code: ` const a = 1; `, }, ], }); ``` --- .prettierrc.json | 1 + packages/eslint-plugin-internal/package.json | 3 +- .../eslint-plugin-internal/src/rules/index.ts | 2 + .../src/rules/plugin-test-formatting.ts | 508 +++++++++++++++++ .../rules/plugin-test-formatting.test.ts | 518 ++++++++++++++++++ packages/eslint-plugin/tests/RuleTester.ts | 14 +- .../eslint-plugin/tools/generate-configs.ts | 2 +- 7 files changed, 1045 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts diff --git a/.prettierrc.json b/.prettierrc.json index bf357fbbc08..a20502b7f06 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,4 @@ { + "singleQuote": true, "trailingComma": "all" } diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 2a66b048492..8e250384428 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -12,6 +12,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.26.0" + "@typescript-eslint/experimental-utils": "2.26.0", + "prettier": "*" } } diff --git a/packages/eslint-plugin-internal/src/rules/index.ts b/packages/eslint-plugin-internal/src/rules/index.ts index f781e01e6df..eb8d8efe852 100644 --- a/packages/eslint-plugin-internal/src/rules/index.ts +++ b/packages/eslint-plugin-internal/src/rules/index.ts @@ -1,9 +1,11 @@ import noTypescriptDefaultImport from './no-typescript-default-import'; import noTypescriptEstreeImport from './no-typescript-estree-import'; +import pluginTestFormatting from './plugin-test-formatting'; import preferASTTypesEnum from './prefer-ast-types-enum'; export default { 'no-typescript-default-import': noTypescriptDefaultImport, 'no-typescript-estree-import': noTypescriptEstreeImport, + 'plugin-test-formatting': pluginTestFormatting, 'prefer-ast-types-enum': preferASTTypesEnum, }; diff --git a/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts new file mode 100644 index 00000000000..bc26d9d9d9a --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts @@ -0,0 +1,508 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import { format, resolveConfig } from 'prettier'; +import { createRule } from '../util'; + +/* +The strings that are used for eslint plugins will not be checked for formatting. +This can lead to diff noise as one contributor adjusts formatting, uses different quotes, etc. + +This rule just enforces the following: +- all code samples are formatted with prettier +- all single line tests do not use backticks +- all multiline tests have: + - no code on the first line + - no code on the last line + - the closing backtick indentation === property indentation + - one of the following indentations: + - no indentation at all + - indentation of 1 + object indent + +eg: +[ + 'const a = 1;', + ` +const a = 1; + `, + ` + const a = 1; + `, + { + code: 'const a = 1', + } + { + code: ` +const a = 1; + `, + } + { + code: ` + const a = 1; + `, + } +] +*/ + +const prettierConfig = resolveConfig.sync(__dirname) ?? {}; +const START_OF_LINE_WHITESPACE_MATCHER = /^([ ]*)/; +const BACKTICK_REGEX = /`/g; +const TEMPLATE_EXPR_OPENER = /\$\{/g; + +function getExpectedIndentForNode( + node: TSESTree.Node, + sourceCodeLines: string[], +): number { + const lineIdx = node.loc.start.line - 1; + const indent = START_OF_LINE_WHITESPACE_MATCHER.exec( + sourceCodeLines[lineIdx], + )![1]; + return indent.length; +} +function doIndent(line: string, indent: number): string { + for (let i = 0; i < indent; i += 1) { + line = ' ' + line; + } + return line; +} + +function getQuote(code: string): "'" | '"' | null { + const hasSingleQuote = code.includes("'"); + const hasDoubleQuote = code.includes('"'); + if (hasSingleQuote && hasDoubleQuote) { + // be lazy and make them fix and escape the quotes manually + return null; + } + + return hasSingleQuote ? '"' : "'"; +} + +function escapeTemplateString(code: string): string { + let fixed = code; + fixed = fixed.replace(BACKTICK_REGEX, '\\`'); + fixed = fixed.replace(TEMPLATE_EXPR_OPENER, '\\${'); + return fixed; +} + +type Options = [ + { + // This option exists so that rules like type-annotation-spacing can exist without every test needing a prettier-ignore + formatWithPrettier?: boolean; + }, +]; + +type MessageIds = + | 'invalidFormatting' + | 'invalidFormattingErrorTest' + | 'singleLineQuotes' + | 'templateLiteralEmptyEnds' + | 'templateLiteralLastLineIndent' + | 'templateStringRequiresIndent' + | 'templateStringMinimumIndent' + | 'prettierException'; + +export default createRule({ + name: 'plugin-test-formatting', + meta: { + type: 'problem', + docs: { + description: `Enforces that eslint-plugin test snippets are correctly formatted`, + category: 'Stylistic Issues', + recommended: 'error', + }, + fixable: 'code', + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + formatWithPrettier: { + type: 'boolean', + }, + }, + }, + ], + messages: { + invalidFormatting: + 'This snippet should be formatted correctly. Use the fixer to format the code.', + invalidFormattingErrorTest: + 'This snippet should be formatted correctly. Use the fixer to format the code. Note that the automated fixer may break your test locations.', + singleLineQuotes: 'Use quotes (\' or ") for single line tests.', + templateLiteralEmptyEnds: + 'Template literals must start and end with an empty line.', + templateLiteralLastLineIndent: + 'The closing line of the template literal must be indented to align with its parent.', + templateStringRequiresIndent: + 'Test code should either have no indent, or be indented {{indent}} spaces.', + templateStringMinimumIndent: + 'Test code should be indented at least {{indent}} spaces.', + prettierException: + 'Prettier was unable to format this snippet: {{message}}', + }, + }, + defaultOptions: [ + { + formatWithPrettier: true, + }, + ], + create(context, [{ formatWithPrettier }]) { + const sourceCode = context.getSourceCode(); + + function prettierFormat( + code: string, + location: TSESTree.Node, + ): string | null { + if (formatWithPrettier === false) { + return null; + } + + try { + return format(code, { + ...prettierConfig, + parser: 'typescript', + }).trimRight(); // prettier will insert a new line at the end of the code + } catch (ex) { + // adapted from https://github.com/prettier/eslint-plugin-prettier/blob/185b1064d3dd674538456fb2fad97fbfcde49e0d/eslint-plugin-prettier.js#L242-L257 + if (!(ex instanceof SyntaxError)) { + throw ex; + } + const err = ex as Error & { + codeFrame: string; + loc?: unknown; + }; + + let message = err.message; + + if (err.codeFrame) { + message = message.replace(`\n${err.codeFrame}`, ''); + } + if (err.loc) { + message = message.replace(/ \(\d+:\d+\)$/, ''); + } + + context.report({ + node: location, + messageId: 'prettierException', + data: { + message, + }, + }); + return null; + } + } + + function checkExpression(node: TSESTree.Node, isErrorTest: boolean): void { + switch (node.type) { + case AST_NODE_TYPES.Literal: + checkLiteral(node, isErrorTest); + break; + + case AST_NODE_TYPES.TemplateLiteral: + checkTemplateLiteral(node, isErrorTest); + break; + + case AST_NODE_TYPES.TaggedTemplateExpression: + checkTaggedTemplateExpression(node, isErrorTest); + break; + + case AST_NODE_TYPES.CallExpression: + checkCallExpression(node, isErrorTest); + break; + } + } + + function checkLiteral( + literal: TSESTree.Literal, + isErrorTest: boolean, + quoteIn?: string, + ): void { + if (typeof literal.value === 'string') { + const output = prettierFormat(literal.value, literal); + if (output && output !== literal.value) { + context.report({ + node: literal, + messageId: isErrorTest + ? 'invalidFormattingErrorTest' + : 'invalidFormatting', + fix(fixer) { + if (output.includes('\n')) { + // formatted string is multiline, then have to use backticks + return fixer.replaceText( + literal, + `\`${escapeTemplateString(output)}\``, + ); + } + + const quote = quoteIn ?? getQuote(output); + if (quote == null) { + return null; + } + + return fixer.replaceText(literal, `${quote}${output}${quote}`); + }, + }); + } + } + } + + function checkTemplateLiteral( + literal: TSESTree.TemplateLiteral, + isErrorTest: boolean, + isNoFormatTagged = false, + ): void { + if (literal.quasis.length > 1) { + // ignore template literals with ${expressions} for simplicity + return; + } + + const text = literal.quasis[0].value.cooked; + + if (literal.loc.end.line === literal.loc.start.line) { + // don't use template strings for single line tests + return context.report({ + node: literal, + messageId: 'singleLineQuotes', + fix(fixer) { + const quote = getQuote(text); + if (quote == null) { + return null; + } + + return [ + fixer.replaceTextRange( + [literal.range[0], literal.range[0] + 1], + quote, + ), + fixer.replaceTextRange( + [literal.range[1] - 1, literal.range[1]], + quote, + ), + ]; + }, + }); + } + + const lines = text.split('\n'); + const lastLine = lines[lines.length - 1]; + // prettier will trim out the end of line on save, but eslint will check before then + const isStartEmpty = lines[0].trimRight() === ''; + // last line can be indented + const isEndEmpty = lastLine.trimLeft() === ''; + if (!isStartEmpty || !isEndEmpty) { + // multiline template strings must have an empty first/last line + return context.report({ + node: literal, + messageId: 'templateLiteralEmptyEnds', + *fix(fixer) { + if (!isStartEmpty) { + yield fixer.replaceTextRange( + [literal.range[0], literal.range[0] + 1], + '`\n', + ); + } + + if (!isEndEmpty) { + yield fixer.replaceTextRange( + [literal.range[1] - 1, literal.range[1]], + '\n`', + ); + } + }, + }); + } + + const parentIndent = getExpectedIndentForNode(literal, sourceCode.lines); + if (lastLine.length !== parentIndent) { + return context.report({ + node: literal, + messageId: 'templateLiteralLastLineIndent', + fix(fixer) { + return fixer.replaceTextRange( + [literal.range[1] - lastLine.length - 1, literal.range[1]], + doIndent('`', parentIndent), + ); + }, + }); + } + + // remove the empty lines + lines.pop(); + lines.shift(); + + // +2 because we expect the string contents are indented one level + const expectedIndent = parentIndent + 2; + + const firstLineIndent = START_OF_LINE_WHITESPACE_MATCHER.exec( + lines[0], + )![1]; + const requiresIndent = firstLineIndent.length > 0; + if (requiresIndent) { + if (firstLineIndent.length !== expectedIndent) { + return context.report({ + node: literal, + messageId: 'templateStringRequiresIndent', + data: { + indent: expectedIndent, + }, + }); + } + + // quick-and-dirty validation that lines are roughly indented correctly + for (const line of lines) { + if (line.length === 0) { + // empty lines are valid + continue; + } + + const matches = START_OF_LINE_WHITESPACE_MATCHER.exec(line)!; + + const indent = matches[1]; + if (indent.length < expectedIndent) { + return context.report({ + node: literal, + messageId: 'templateStringMinimumIndent', + data: { + indent: expectedIndent, + }, + }); + } + } + + // trim the lines to remove expectedIndent characters from the start + // this makes it easier to check formatting + for (let i = 0; i < lines.length; i += 1) { + lines[i] = lines[i].substring(expectedIndent); + } + } + + if (isNoFormatTagged) { + return; + } + + const code = lines.join('\n'); + const formatted = prettierFormat(code, literal); + if (formatted && formatted !== code) { + const formattedIndented = requiresIndent + ? formatted + .split('\n') + .map(l => doIndent(l, expectedIndent)) + .join('\n') + : formatted; + + return context.report({ + node: literal, + messageId: isErrorTest + ? 'invalidFormattingErrorTest' + : 'invalidFormatting', + fix(fixer) { + return fixer.replaceText( + literal, + `\`\n${escapeTemplateString(formattedIndented)}\n${doIndent( + '', + parentIndent, + )}\``, + ); + }, + }); + } + } + + function isNoFormatTemplateTag(tag: TSESTree.Expression): boolean { + return tag.type === AST_NODE_TYPES.Identifier && tag.name === 'noFormat'; + } + + function checkTaggedTemplateExpression( + expr: TSESTree.TaggedTemplateExpression, + isErrorTest: boolean, + ): void { + if (!isNoFormatTemplateTag(expr.tag)) { + return; + } + + if (expr.loc.start.line === expr.loc.end.line) { + // all we do on single line test cases is check format, but there's no formatting to do + return; + } + + checkTemplateLiteral( + expr.quasi, + isErrorTest, + isNoFormatTemplateTag(expr.tag), + ); + } + + function checkCallExpression( + callExpr: TSESTree.CallExpression, + isErrorTest: boolean, + ): void { + if (callExpr.callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + const memberExpr = callExpr.callee; + // handle cases like 'aa'.trimRight and `aa`.trimRight() + checkExpression(memberExpr.object, isErrorTest); + } + + function checkInvalidTest( + test: TSESTree.ObjectExpression, + isErrorTest = true, + ): void { + for (const prop of test.properties) { + if ( + prop.type !== AST_NODE_TYPES.Property || + prop.computed || + prop.key.type !== AST_NODE_TYPES.Identifier + ) { + continue; + } + + if (prop.key.name === 'code' || prop.key.name === 'output') { + checkExpression(prop.value, isErrorTest); + } + } + } + + function checkValidTest(tests: TSESTree.ArrayExpression): void { + for (const test of tests.elements) { + switch (test.type) { + case AST_NODE_TYPES.ObjectExpression: + // delegate object-style tests to the invalid checker + checkInvalidTest(test, false); + break; + + default: + checkExpression(test, false); + break; + } + } + } + + const invalidTestsSelectorPath = [ + AST_NODE_TYPES.CallExpression, + AST_NODE_TYPES.ObjectExpression, + 'Property[key.name = "invalid"]', + AST_NODE_TYPES.ArrayExpression, + AST_NODE_TYPES.ObjectExpression, + ]; + + return { + // valid + 'CallExpression > ObjectExpression > Property[key.name = "valid"] > ArrayExpression': checkValidTest, + // invalid - errors + [invalidTestsSelectorPath.join(' > ')]: checkInvalidTest, + // invalid - suggestions + [[ + ...invalidTestsSelectorPath, + 'Property[key.name = "errors"]', + AST_NODE_TYPES.ArrayExpression, + AST_NODE_TYPES.ObjectExpression, + 'Property[key.name = "suggestions"]', + AST_NODE_TYPES.ArrayExpression, + AST_NODE_TYPES.ObjectExpression, + ].join(' > ')]: checkInvalidTest, + // special case for our batchedSingleLineTests utility + 'CallExpression[callee.name = "batchedSingleLineTests"] > ObjectExpression': checkInvalidTest, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts b/packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts new file mode 100644 index 00000000000..8d7e0b2c1d8 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts @@ -0,0 +1,518 @@ +import rule from '../../src/rules/plugin-test-formatting'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, +}); + +const CODE_INDENT = ' '; +const PARENT_INDENT = ' '; +function wrap(strings: TemplateStringsArray, ...keys: string[]): string { + const lastIndex = strings.length - 1; + const code = + strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') + + strings[lastIndex]; + return ` +ruleTester.run({ + valid: [ + { + code: ${code}, + }, + ], +}); + `; +} +function wrapWithOutput( + strings: TemplateStringsArray, + ...keys: string[] +): string { + const lastIndex = strings.length - 1; + const code = + strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') + + strings[lastIndex]; + return ` +ruleTester.run({ + invalid: [ + { + code: ${code}, + output: ${code}, + }, + ], +}); + `; +} + +ruleTester.run('plugin-test-formatting', rule, { + valid: [ + // sanity check for valid tests non-object style + ` +ruleTester.run({ + valid: [ + 'const a = 1;', + \` + const a = 1; + \`, + \` +const a = 1; + \`, + noFormat\`const x=1;\`, + ], +}); + `, + wrap`'const a = 1;'`, + wrap`\` +${CODE_INDENT}const a = 1; +${PARENT_INDENT}\``, + wrap`\` +const a = 1; +${PARENT_INDENT}\``, + wrap`noFormat\`const a = 1;\``, + // sanity check suggestion validation + ` + ruleTester.run({ + invalid: [ + { + code: 'const a = 1;', + output: 'const a = 1;', + errors: [ + { + messageId: 'foo', + suggestions: [ + { + messageId: 'bar', + output: 'const a = 1;', + }, + ], + } + ] + }, + { + code: \` + const a = 1; + \`, + output: \` + const a = 1; + \`, + errors: [ + { + messageId: 'foo', + suggestions: [ + { + messageId: 'bar', + output: \` + const a = 1; + \`, + }, + ], + } + ] + }, + { + code: \` +const a = 1; + \`, + output: \` +const a = 1; + \`, + errors: [ + { + messageId: 'foo', + suggestions: [ + { + messageId: 'bar', + output: \` +const a = 1; + \`, + }, + ], + } + ] + }, + ], + }); + `, + + // test the only option + { + code: wrap`'const x=1;'`, + options: [ + { + formatWithPrettier: false, + }, + ], + }, + + // empty linems are valid when everything else is indented + wrap`\` +${CODE_INDENT}const a = 1; + +${CODE_INDENT}const b = 1; +${PARENT_INDENT}\``, + ], + invalid: [ + // Literal + { + code: wrap`'const a=1;'`, + output: wrap`'const a = 1;'`, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`'const a="1";'`, + output: wrap`"const a = '1';"`, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`"const a='1';"`, + output: wrap`"const a = '1';"`, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`'for (const x of y) {}'`, + output: wrap`\`for (const x of y) { +}\``, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`'for (const x of \`asdf\`) {}'`, + // make sure it escapes the backticks + output: wrap`\`for (const x of \\\`asdf\\\`) { +}\``, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + // TemplateLiteral + // singleLineQuotes + { + code: wrap`\`const a = 1;\``, + output: wrap`'const a = 1;'`, + errors: [ + { + messageId: 'singleLineQuotes', + }, + ], + }, + { + code: wrap`\`const a = '1'\``, + output: wrap`"const a = '1'"`, + errors: [ + { + messageId: 'singleLineQuotes', + }, + ], + }, + { + code: wrap`\`const a = "1";\``, + output: wrap`'const a = "1";'`, + errors: [ + { + messageId: 'singleLineQuotes', + }, + ], + }, + // templateLiteralEmptyEnds + { + code: wrap`\`const a = "1"; +${PARENT_INDENT}\``, + output: wrap`\` +const a = "1"; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'templateLiteralEmptyEnds', + }, + ], + }, + { + code: wrap`\` +${CODE_INDENT}const a = "1";\``, + output: wrap`\` +${CODE_INDENT}const a = "1"; +\``, + errors: [ + { + messageId: 'templateLiteralEmptyEnds', + }, + ], + }, + { + code: wrap`\`const a = "1"; +${CODE_INDENT}const b = "2";\``, + output: wrap`\` +const a = "1"; +${CODE_INDENT}const b = "2"; +\``, + errors: [ + { + messageId: 'templateLiteralEmptyEnds', + }, + ], + }, + // templateLiteralLastLineIndent + { + code: wrap`\` +${CODE_INDENT}const a = "1"; +\``, + output: wrap`\` +${CODE_INDENT}const a = "1"; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'templateLiteralLastLineIndent', + }, + ], + }, + { + code: wrap`\` +${CODE_INDENT}const a = "1"; + \``, + output: wrap`\` +${CODE_INDENT}const a = "1"; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'templateLiteralLastLineIndent', + }, + ], + }, + // templateStringRequiresIndent + { + code: wrap`\` + const a = "1"; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'templateStringRequiresIndent', + data: { + indent: CODE_INDENT.length, + }, + }, + ], + }, + { + code: ` +ruleTester.run({ + valid: [ + \` + const a = "1"; + \`, + ], +}); + `, + errors: [ + { + messageId: 'templateStringRequiresIndent', + data: { + indent: 6, + }, + }, + ], + }, + // templateStringMinimumIndent + { + code: wrap`\` +${CODE_INDENT}const a = "1"; + const b = "2"; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'templateStringMinimumIndent', + data: { + indent: CODE_INDENT.length, + }, + }, + ], + }, + // invalidFormatting + { + code: wrap`\` +${CODE_INDENT}const a="1"; +${CODE_INDENT} const b = "2"; +${PARENT_INDENT}\``, + output: wrap`\` +${CODE_INDENT}const a = '1'; +${CODE_INDENT}const b = '2'; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`\` +${CODE_INDENT}const a=\\\`\\\${a}\\\`; +${PARENT_INDENT}\``, + // make sure it escapes backticks + output: wrap`\` +${CODE_INDENT}const a = \\\`\\\${a}\\\`; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + + // sanity check that it runs on both output and code properties + { + code: wrapWithOutput`\` +${CODE_INDENT}const a="1"; +${CODE_INDENT} const b = "2"; +${PARENT_INDENT}\``, + output: wrapWithOutput`\` +${CODE_INDENT}const a = '1'; +${CODE_INDENT}const b = '2'; +${PARENT_INDENT}\``, + errors: [ + { + messageId: 'invalidFormattingErrorTest', + }, + { + messageId: 'invalidFormattingErrorTest', + }, + ], + }, + + // sanity check that it handles suggestion output + { + code: ` +ruleTester.run({ + valid: [], + invalid: [ + { + code: 'const x=1;', + errors: [ + { + messageId: 'foo', + suggestions: [ + { + messageId: 'bar', + output: 'const x=1;', + }, + ], + }, + ], + }, + ], +}); + `, + errors: [ + { + messageId: 'invalidFormattingErrorTest', + }, + { + messageId: 'invalidFormattingErrorTest', + }, + ], + }, + + // sanity check that it runs on all tests + { + code: ` +ruleTester.run({ + valid: [ + { + code: \`foo\`, + }, + { + code: \`foo +\`, + }, + { + code: \` + foo\`, + }, + ], + invalid: [ + { + code: \`foo\`, + }, + { + code: \`foo +\`, + }, + { + code: \` + foo\`, + }, + ], +}); + `, + errors: [ + { + messageId: 'singleLineQuotes', + }, + { + messageId: 'templateLiteralEmptyEnds', + }, + { + messageId: 'templateLiteralEmptyEnds', + }, + { + messageId: 'singleLineQuotes', + }, + { + messageId: 'templateLiteralEmptyEnds', + }, + { + messageId: 'templateLiteralEmptyEnds', + }, + ], + }, + + // handles prettier errors + { + code: wrap`'const x = ";'`, + errors: [ + { + messageId: 'prettierException', + data: { + message: 'Unterminated string literal.', + }, + }, + ], + }, + + // checks tests with .trimRight calls + { + code: wrap`'const a=1;'.trimRight()`, + output: wrap`'const a = 1;'.trimRight()`, + errors: [ + { + messageId: 'invalidFormatting', + }, + ], + }, + { + code: wrap`\`const a = "1"; +${CODE_INDENT}\`.trimRight()`, + errors: [ + { + messageId: 'templateLiteralEmptyEnds', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index c650c83449c..1774200db32 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -89,4 +89,16 @@ afterAll(() => { clearCaches(); }); -export { RuleTester, getFixturesRootDir, batchedSingleLineTests }; +/** + * Simple no-op tag to mark code samples as "should not format with prettier" + * for the internal/plugin-test-formatting lint rule + */ +function noFormat(strings: TemplateStringsArray, ...keys: string[]): string { + const lastIndex = strings.length - 1; + return ( + strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') + + strings[lastIndex] + ); +} + +export { batchedSingleLineTests, getFixturesRootDir, noFormat, RuleTester }; diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index d35fd7362bc..e9d2a8be762 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -5,7 +5,7 @@ import path from 'path'; import { format, resolveConfig } from 'prettier'; import rules from '../src/rules'; -const prettierConfig = resolveConfig(__dirname); +const prettierConfig = resolveConfig.sync(__dirname); interface LinterConfigRules { [name: string]: