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]: