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 index 25e95fef3ea3..280d3fd67f8e 100644 --- a/packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts @@ -206,16 +206,18 @@ const test = [ }, { code: wrap`'for (const x of y) {}'`, - output: ` -ruleTester.run({ - valid: [ - { - code: \`for (const x of y) { -}\`, - }, - ], -}); - `, + output: [ + wrap`\`for (const x of y) { +}\``, + wrap`\` +for (const x of y) { +} +\``, + wrap`\` +for (const x of y) { +} +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'invalidFormatting', @@ -225,16 +227,18 @@ ruleTester.run({ { code: wrap`'for (const x of \`asdf\`) {}'`, // make sure it escapes the backticks - output: ` -ruleTester.run({ - valid: [ - { - code: \`for (const x of \\\`asdf\\\`) { -}\`, - }, - ], -}); - `, + output: [ + wrap`\`for (const x of \\\`asdf\\\`) { +}\``, + wrap`\` +for (const x of \\\`asdf\\\`) { +} +\``, + wrap`\` +for (const x of \\\`asdf\\\`) { +} +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'invalidFormatting', @@ -254,15 +258,7 @@ ruleTester.run({ }, { code: wrap`\`const a = '1'\``, - output: ` -ruleTester.run({ - valid: [ - { - code: "const a = '1'", - }, - ], -}); - `, + output: [wrap`"const a = '1'"`, wrap`"const a = '1';"`], errors: [ { messageId: 'singleLineQuotes', @@ -271,15 +267,7 @@ ruleTester.run({ }, { code: wrap`\`const a = "1";\``, - output: ` -ruleTester.run({ - valid: [ - { - code: 'const a = "1";', - }, - ], -}); - `, + output: [wrap`'const a = "1";'`, wrap`"const a = '1';"`], errors: [ { messageId: 'singleLineQuotes', @@ -290,17 +278,14 @@ ruleTester.run({ { code: wrap`\`const a = "1"; ${PARENT_INDENT}\``, - output: ` -ruleTester.run({ - valid: [ - { - code: \` + output: [ + wrap`\` const a = "1"; - \`, - }, - ], -}); - `, +${PARENT_INDENT}\``, + wrap`\` +const a = '1'; +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'templateLiteralEmptyEnds', @@ -310,17 +295,17 @@ const a = "1"; { code: wrap`\` ${CODE_INDENT}const a = "1";\``, - output: ` -ruleTester.run({ - valid: [ - { - code: \` - const a = "1"; -\`, - }, - ], -}); - `, + output: [ + wrap`\` +${CODE_INDENT}const a = "1"; +\``, + wrap`\` +${CODE_INDENT}const a = "1"; +${PARENT_INDENT}\``, + wrap`\` +${CODE_INDENT}const a = '1'; +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'templateLiteralEmptyEnds', @@ -330,18 +315,20 @@ ruleTester.run({ { code: wrap`\`const a = "1"; ${CODE_INDENT}const b = "2";\``, - output: ` -ruleTester.run({ - valid: [ - { - code: \` + output: [ + wrap`\` const a = "1"; - const b = "2"; -\`, - }, - ], -}); - `, +${CODE_INDENT}const b = "2"; +\``, + wrap`\` +const a = "1"; +${CODE_INDENT}const b = "2"; +${PARENT_INDENT}\``, + wrap`\` +const a = '1'; +const b = '2'; +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'templateLiteralEmptyEnds', @@ -353,17 +340,14 @@ const a = "1"; code: wrap`\` ${CODE_INDENT}const a = "1"; \``, - output: ` -ruleTester.run({ - valid: [ - { - code: \` - const a = "1"; - \`, - }, - ], -}); - `, + output: [ + wrap`\` +${CODE_INDENT}const a = "1"; +${PARENT_INDENT}\``, + wrap`\` +${CODE_INDENT}const a = '1'; +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'templateLiteralLastLineIndent', @@ -374,17 +358,14 @@ ruleTester.run({ code: wrap`\` ${CODE_INDENT}const a = "1"; \``, - output: ` -ruleTester.run({ - valid: [ - { - code: \` - const a = "1"; - \`, - }, - ], -}); - `, + output: [ + wrap`\` +${CODE_INDENT}const a = "1"; +${PARENT_INDENT}\``, + wrap`\` +${CODE_INDENT}const a = '1'; +${PARENT_INDENT}\``, + ], errors: [ { messageId: 'templateLiteralLastLineIndent', @@ -555,7 +536,8 @@ ruleTester.run({ ], }); `, - output: ` + output: [ + ` ruleTester.run({ valid: [ { @@ -589,6 +571,75 @@ foo ], }); `, + ` +ruleTester.run({ + valid: [ + { + code: 'foo;', + }, + { + code: \` +foo + \`, + }, + { + code: \` + foo + \`, + }, + ], + invalid: [ + { + code: 'foo;', + }, + { + code: \` +foo + \`, + }, + { + code: \` + foo + \`, + }, + ], +}); + `, + ` +ruleTester.run({ + valid: [ + { + code: 'foo;', + }, + { + code: \` +foo; + \`, + }, + { + code: \` + foo + \`, + }, + ], + invalid: [ + { + code: 'foo;', + }, + { + code: \` +foo; + \`, + }, + { + code: \` + foo + \`, + }, + ], +}); + `, + ], errors: [ { messageId: 'singleLineQuotes', diff --git a/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts index 628057ac7d17..2e9c35178a48 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts @@ -409,11 +409,16 @@ declare const nested: string, interpolation: string; \`le\${ \`ss\` }\` }\`; `, - output: ` + output: [ + ` \`use\${ \`less\` }\`; `, + ` +\`useless\`; + `, + ], errors: [ { messageId: 'noUselessTemplateExpression', diff --git a/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts b/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts index 06eacb1e31e6..4c02f042b986 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts @@ -409,11 +409,16 @@ declare const nested: string, interpolation: string; \`le\${ \`ss\` }\` }\`; `, - output: ` + output: [ + ` \`use\${ \`less\` }\`; `, + ` +\`useless\`; + `, + ], errors: [ { messageId: 'noUselessTemplateExpression', diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 7ebe8df5a87b..de19117fcbce 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -489,7 +489,7 @@ export class RuleTester extends TestFramework { item: InvalidTestCase | ValidTestCase, ): { messages: Linter.LintMessage[]; - output: string; + outputs: string[]; beforeAST: TSESTree.Program; afterAST: TSESTree.Program; config: RuleTesterConfig; @@ -498,7 +498,6 @@ export class RuleTester extends TestFramework { let config: TesterConfigWithDefaults = merge({}, this.#testerConfig); let code; let filename; - let output; let beforeAST: TSESTree.Program; let afterAST: TSESTree.Program; @@ -621,29 +620,47 @@ export class RuleTester extends TestFramework { // Verify the code. // @ts-expect-error -- we don't define deprecated members on our types const { getComments } = SourceCode.prototype as { getComments: unknown }; - let messages; - - try { - // @ts-expect-error -- we don't define deprecated members on our types - SourceCode.prototype.getComments = getCommentsDeprecation; - messages = this.#linter.verify(code, config, filename); - } finally { - // @ts-expect-error -- we don't define deprecated members on our types - SourceCode.prototype.getComments = getComments; - } - const fatalErrorMessage = messages.find(m => m.fatal); + let initialMessages: Linter.LintMessage[] | null = null; + let messages: Linter.LintMessage[] | null = null; + let fixedResult: SourceCodeFixer.AppliedFixes | null = null; + let passNumber = 0; + const outputs: string[] = []; - assert( - !fatalErrorMessage, - `A fatal parsing error occurred: ${fatalErrorMessage?.message}`, - ); + do { + passNumber++; + + try { + // @ts-expect-error -- we don't define deprecated members on our types + SourceCode.prototype.getComments = getCommentsDeprecation; + messages = this.#linter.verify(code, config, filename); + if (!initialMessages) { + initialMessages = messages; + } + } finally { + // @ts-expect-error -- we don't define deprecated members on our types + SourceCode.prototype.getComments = getComments; + } + if (messages.length === 0) { + break; + } - // Verify if autofix makes a syntax error or not. - if (messages.some(m => m.fix)) { - output = SourceCodeFixer.applyFixes(code, messages).output; + const fatalErrorMessage = messages.find(m => m.fatal); + assert( + !fatalErrorMessage, + `A fatal parsing error occurred: ${fatalErrorMessage?.message}`, + ); + + fixedResult = SourceCodeFixer.applyFixes(code, messages); + if (fixedResult.output === code) { + break; + } + code = fixedResult.output; + outputs.push(code); + + // Verify if autofix makes a syntax error or not. const errorMessageInFix = this.#linter - .verify(output, config, filename) + .verify(fixedResult.output, config, filename) .find(m => m.fatal); assert( @@ -652,24 +669,22 @@ export class RuleTester extends TestFramework { 'A fatal parsing error occurred in autofix.', `Error: ${errorMessageInFix?.message}`, 'Autofix output:', - output, + fixedResult.output, ].join('\n'), ); - } else { - output = code; - } + } while (fixedResult.fixed && passNumber < 10); return { - messages, - output, + config, + filename, + messages: initialMessages, + outputs, // is definitely assigned within the `rule-tester/validate-ast` rule // eslint-disable-next-line @typescript-eslint/no-non-null-assertion beforeAST: beforeAST!, // is definitely assigned within the `rule-tester/validate-ast` rule // eslint-disable-next-line @typescript-eslint/no-non-null-assertion afterAST: cloneDeeplyExcludesParent(afterAST!), - config, - filename, }; } @@ -1101,20 +1116,43 @@ export class RuleTester extends TestFramework { if (hasOwnProperty(item, 'output')) { if (item.output == null) { + if (result.outputs.length) { + assert.strictEqual( + result.outputs[0], + item.code, + 'Expected no autofixes to be suggested.', + ); + } + } else if (typeof item.output === 'string') { + assert(result.outputs.length > 0, 'Expected autofix to be suggested.'); assert.strictEqual( - result.output, - item.code, - 'Expected no autofixes to be suggested', + result.outputs[0], + item.output, + 'Output is incorrect.', ); + if (result.outputs.length) { + assert.deepStrictEqual( + result.outputs, + [item.output], + 'Multiple autofixes are required due to overlapping fix ranges - please use the array form of output to declare all of the expected autofix passes.', + ); + } } else { - assert.strictEqual(result.output, item.output, 'Output is incorrect.'); + assert(result.outputs.length > 0, 'Expected autofix to be suggested.'); + assert.deepStrictEqual( + result.outputs, + item.output, + 'Outputs do not match.', + ); } } else { - assert.strictEqual( - result.output, - item.code, - "The rule fixed the code. Please add 'output' property.", - ); + if (result.outputs.length) { + assert.strictEqual( + result.outputs[0], + item.code, + "The rule fixed the code. Please add 'output' property.", + ); + } } assertASTDidntChange(result.beforeAST, result.afterAST); diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts index 2db16e5d4e9b..2550d0239ab6 100644 --- a/packages/rule-tester/tests/RuleTester.test.ts +++ b/packages/rule-tester/tests/RuleTester.test.ts @@ -76,11 +76,6 @@ const mockedDescribeSkip = jest.mocked(RuleTester.describeSkip); const mockedIt = jest.mocked(RuleTester.it); const _mockedItOnly = jest.mocked(RuleTester.itOnly); const _mockedItSkip = jest.mocked(RuleTester.itSkip); -const runRuleForItemSpy = jest.spyOn( - RuleTester.prototype, - // @ts-expect-error -- method is private - 'runRuleForItem', -) as jest.SpiedFunction; const mockedParserClearCaches = jest.mocked(parser.clearCaches); const EMPTY_PROGRAM: TSESTree.Program = { @@ -92,33 +87,6 @@ const EMPTY_PROGRAM: TSESTree.Program = { tokens: [], range: [0, 0], }; -runRuleForItemSpy.mockImplementation((_1, _2, testCase) => { - return { - messages: - 'errors' in testCase - ? [ - { - column: 0, - line: 0, - message: 'error', - messageId: 'error', - nodeType: AST_NODE_TYPES.Program, - ruleId: 'my-rule', - severity: 2, - source: null, - }, - ] - : [], - output: testCase.code, - afterAST: EMPTY_PROGRAM, - beforeAST: EMPTY_PROGRAM, - config: { parser: '' }, - }; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); const NOOP_RULE: RuleModule<'error'> = { meta: { @@ -134,13 +102,45 @@ const NOOP_RULE: RuleModule<'error'> = { }, }; -function getTestConfigFromCall(): unknown[] { - return runRuleForItemSpy.mock.calls.map(c => { - return { ...c[2], filename: c[2].filename?.replaceAll('\\', '/') }; +describe('RuleTester', () => { + const runRuleForItemSpy = jest.spyOn( + RuleTester.prototype, + // @ts-expect-error -- method is private + 'runRuleForItem', + ) as jest.SpiedFunction; + beforeEach(() => { + jest.clearAllMocks(); + }); + runRuleForItemSpy.mockImplementation((_1, _2, testCase) => { + return { + messages: + 'errors' in testCase + ? [ + { + column: 0, + line: 0, + message: 'error', + messageId: 'error', + nodeType: AST_NODE_TYPES.Program, + ruleId: 'my-rule', + severity: 2, + source: null, + }, + ] + : [], + outputs: [testCase.code], + afterAST: EMPTY_PROGRAM, + beforeAST: EMPTY_PROGRAM, + config: { parser: '' }, + }; }); -} -describe('RuleTester', () => { + function getTestConfigFromCall(): unknown[] { + return runRuleForItemSpy.mock.calls.map(c => { + return { ...c[2], filename: c[2].filename?.replaceAll('\\', '/') }; + }); + } + describe('filenames', () => { it('automatically sets the filename for tests', () => { const ruleTester = new RuleTester({ @@ -172,7 +172,6 @@ describe('RuleTester', () => { { code: 'type-aware parser options should override the constructor config', parserOptions: { - projectService: false, project: 'tsconfig.test-specific.json', tsconfigRootDir: '/set/in/the/test/', }, @@ -222,7 +221,6 @@ describe('RuleTester', () => { "parserOptions": { "disallowAutomaticSingleRunInference": true, "project": "tsconfig.test-specific.json", - "projectService": false, "tsconfigRootDir": "/set/in/the/test/", }, }, @@ -947,3 +945,301 @@ describe('RuleTester', () => { }); }); }); + +describe('RuleTester - multipass fixer', () => { + beforeAll(() => { + jest.restoreAllMocks(); + }); + + describe('without fixes', () => { + const ruleTester = new RuleTester(); + const rule: RuleModule<'error'> = { + meta: { + messages: { + error: 'error', + }, + type: 'problem', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'Identifier[name=foo]'(node): void { + context.report({ + node, + messageId: 'error', + }); + }, + }; + }, + }; + + it('passes with no output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).not.toThrow(); + }); + + it('passes with null output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).not.toThrow(); + }); + + it('throws with string output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: 'bar', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Expected autofix to be suggested.'); + }); + + it('throws with array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['bar', 'baz'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Expected autofix to be suggested.'); + }); + }); + + describe('with single fix', () => { + const ruleTester = new RuleTester(); + const rule: RuleModule<'error'> = { + meta: { + messages: { + error: 'error', + }, + type: 'problem', + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'Identifier[name=foo]'(node): void { + context.report({ + node, + messageId: 'error', + fix: fixer => fixer.replaceText(node, 'bar'), + }); + }, + }; + }, + }; + + it('passes with correct string output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: 'bar', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).not.toThrow(); + }); + + it('passes with correct array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['bar'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).not.toThrow(); + }); + + it('throws with no output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow("The rule fixed the code. Please add 'output' property."); + }); + + it('throws with null output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: null, + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Expected no autofixes to be suggested.'); + }); + + it('throws with incorrect array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['bar', 'baz'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Outputs do not match.'); + }); + + it('throws with incorrect string output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: 'baz', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Output is incorrect.'); + }); + }); + + describe('with multiple fixes', () => { + const ruleTester = new RuleTester(); + const rule: RuleModule<'error'> = { + meta: { + messages: { + error: 'error', + }, + type: 'problem', + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + 'Identifier[name=foo]'(node): void { + context.report({ + node, + messageId: 'error', + fix: fixer => fixer.replaceText(node, 'bar'), + }); + }, + 'Identifier[name=bar]'(node): void { + context.report({ + node, + messageId: 'error', + fix: fixer => fixer.replaceText(node, 'baz'), + }); + }, + }; + }, + }; + + it('passes with correct array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['bar', 'baz'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).not.toThrow(); + }); + + it('throws with string output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: 'bar', + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow( + 'Multiple autofixes are required due to overlapping fix ranges - please use the array form of output to declare all of the expected autofix passes.', + ); + }); + + it('throws with incorrect array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['bar'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Outputs do not match.'); + }); + + it('throws with incorrectly ordered array output', () => { + expect(() => { + ruleTester.run('my-rule', rule, { + valid: [], + invalid: [ + { + code: 'foo', + output: ['baz', 'bar'], + errors: [{ messageId: 'error' }], + }, + ], + }); + }).toThrow('Outputs do not match.'); + }); + }); +}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy