diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index fc792f622a..7b1a239f2b 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -478,6 +478,11 @@ "const" : "BooleanExpressionToFalse", "title": "BooleanExpressionToFalseMutator", "description": "Replace ```var x = a > b ? 1 : 2;``` with ```var x = false ? 1 : 2;```." + }, + { + "const" : "SwitchToEmpty", + "title": "SwitchToEmptyMutator", + "description": "Replace ```switch(x) with switch()```." } ] }, diff --git a/packages/instrumenter/src/mutators/conditional-expression-mutator.ts b/packages/instrumenter/src/mutators/conditional-expression-mutator.ts index dd0fab6833..b6102f03b3 100644 --- a/packages/instrumenter/src/mutators/conditional-expression-mutator.ts +++ b/packages/instrumenter/src/mutators/conditional-expression-mutator.ts @@ -8,43 +8,85 @@ const booleanOperators = Object.freeze(['!=', '!==', '&&', '<', '<=', '==', '=== const { types } = babel; +const conditionalReplacements = Object.assign({ + BooleanExpressionToFalse: { replacementOperator: types.booleanLiteral(false), mutatorName: 'BooleanExpressionToFalse' }, + BooleanExpressionToTrue: { replacementOperator: types.booleanLiteral(true), mutatorName: 'BooleanExpressionToTrue' }, + DoWhileLoopToFalse: { replacementOperator: types.booleanLiteral(false), mutatorName: 'DoWhileLoopToFalse' }, + ForLoopToFalse: { replacementOperator: types.booleanLiteral(false), mutatorName: 'ForLoopToFalse' }, + IfToFalse: { replacementOperator: types.booleanLiteral(false), mutatorName: 'IfToFalse' }, + IfToTrue: { replacementOperator: types.booleanLiteral(true), mutatorName: 'IfToTrue' }, + WhileLoopToFalse: { replacementOperator: types.booleanLiteral(false), mutatorName: 'WhileLoopToFalse' }, + SwitchToEmpty: { replacementOperator: [], mutatorName: 'SwitchToEmpty' }, +} as const); + export const conditionalExpressionMutator: NodeMutator = { name: 'ConditionalExpression', - *mutate(path) { + *mutate(path, operations) { if (isTestOfLoop(path)) { - yield types.booleanLiteral(false); + if ( + isTestOfWhileLoop(path) && + (operations === undefined || operations.includes(conditionalReplacements.WhileLoopToFalse.mutatorName as string)) + ) { + yield conditionalReplacements.WhileLoopToFalse.replacementOperator; + } + + if ( + isTestOfDoWhileLoop(path) && + (operations === undefined || operations.includes(conditionalReplacements.DoWhileLoopToFalse.mutatorName as string)) + ) { + yield conditionalReplacements.DoWhileLoopToFalse.replacementOperator; + } + if (isTestOfForLoop(path) && (operations === undefined || operations.includes(conditionalReplacements.ForLoopToFalse.mutatorName as string))) { + yield conditionalReplacements.ForLoopToFalse.replacementOperator; + } } else if (isTestOfCondition(path)) { - yield types.booleanLiteral(true); - yield types.booleanLiteral(false); + if (operations === undefined || operations.includes(conditionalReplacements.IfToTrue.mutatorName as string)) { + yield conditionalReplacements.IfToTrue.replacementOperator; + } + if (operations === undefined || operations.includes(conditionalReplacements.IfToFalse.mutatorName as string)) { + yield conditionalReplacements.IfToFalse.replacementOperator; + } } else if (isBooleanExpression(path)) { if (path.parent?.type === 'LogicalExpression') { // For (x || y), do not generate the (true || y) mutation as it // has the same behavior as the (true) mutator, handled in the // isTestOfCondition branch above if (path.parent.operator === '||') { - yield types.booleanLiteral(false); + if (operations === undefined || operations.includes(conditionalReplacements.BooleanExpressionToFalse.mutatorName as string)) { + yield conditionalReplacements.BooleanExpressionToFalse.replacementOperator; + } return; } // For (x && y), do not generate the (false && y) mutation as it // has the same behavior as the (false) mutator, handled in the // isTestOfCondition branch above if (path.parent.operator === '&&') { - yield types.booleanLiteral(true); + if (operations === undefined || operations.includes(conditionalReplacements.BooleanExpressionToTrue.mutatorName as string)) { + yield conditionalReplacements.BooleanExpressionToTrue.replacementOperator; + } return; } } - yield types.booleanLiteral(true); - yield types.booleanLiteral(false); + if (operations === undefined || operations.includes(conditionalReplacements.BooleanExpressionToTrue.mutatorName as string)) { + yield conditionalReplacements.BooleanExpressionToTrue.replacementOperator; + } + if (operations === undefined || operations.includes(conditionalReplacements.BooleanExpressionToFalse.mutatorName as string)) { + yield conditionalReplacements.BooleanExpressionToFalse.replacementOperator; + } } else if (path.isForStatement() && !path.node.test) { - const replacement = deepCloneNode(path.node); - replacement.test = types.booleanLiteral(false); - yield replacement; + if (operations === undefined || operations.includes(conditionalReplacements.ForLoopToFalse.mutatorName as string)) { + const replacement = deepCloneNode(path.node); + replacement.test = conditionalReplacements.ForLoopToFalse.replacementOperator; + yield replacement; + } } else if (path.isSwitchCase() && path.node.consequent.length > 0) { // if not a fallthrough case - const replacement = deepCloneNode(path.node); - replacement.consequent = []; - yield replacement; + if (operations === undefined || operations.includes(conditionalReplacements.SwitchToEmpty.mutatorName as string)) { + const replacement = deepCloneNode(path.node); + replacement.consequent = conditionalReplacements.SwitchToEmpty.replacementOperator; + yield replacement; + } } }, }; @@ -57,6 +99,30 @@ function isTestOfLoop(path: NodePath): boolean { return (parentPath.isForStatement() || parentPath.isWhileStatement() || parentPath.isDoWhileStatement()) && parentPath.node.test === path.node; } +function isTestOfWhileLoop(path: NodePath): boolean { + const { parentPath } = path; + if (!parentPath) { + return false; + } + return parentPath.isWhileStatement() && parentPath.node.test === path.node; +} + +function isTestOfForLoop(path: NodePath): boolean { + const { parentPath } = path; + if (!parentPath) { + return false; + } + return parentPath.isForStatement() && parentPath.node.test === path.node; +} + +function isTestOfDoWhileLoop(path: NodePath): boolean { + const { parentPath } = path; + if (!parentPath) { + return false; + } + return parentPath.isDoWhileStatement() && parentPath.node.test === path.node; +} + function isTestOfCondition(path: NodePath): boolean { const { parentPath } = path; if (!parentPath) { diff --git a/packages/instrumenter/test/unit/mutators/conditional-expression-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/conditional-expression-mutator.spec.ts index 68b060c2f3..edc7a7e6fe 100644 --- a/packages/instrumenter/test/unit/mutators/conditional-expression-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/conditional-expression-mutator.spec.ts @@ -1,8 +1,12 @@ import { expect } from 'chai'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; import { conditionalExpressionMutator as sut } from '../../../src/mutators/conditional-expression-mutator.js'; +const conditionLevel: string[] = ['ForLoopToFalse', 'IfToFalse', 'IfToTrue', 'SwitchToEmpty']; +const conditionLevel2: string[] = ['WhileLoopToFalse', 'BooleanExpressionToFalse', 'DoWhileLoopToFalse', 'BooleanExpressionToTrue']; +const conditionLevel3 = undefined; + describe(sut.name, () => { it('should have name "ConditionalExpression"', () => { expect(sut.name).eq('ConditionalExpression'); @@ -140,4 +144,44 @@ describe(sut.name, () => { it('should mutate the expression of a while statement', () => { expectJSMutation(sut, 'while(a < b) { console.log(); }', 'while(false) { console.log(); }'); }); + + it('should only mutate for, if and switch statement', () => { + expectJSMutationWithLevel( + sut, + conditionLevel, + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}', + 'for (var i = 0; false; i++) { };if(x > 2); switch (x) {case 0: 2}', // mutates for loop + 'for (var i = 0; i < 10; i++) { };if(false); switch (x) {case 0: 2}', // mutates if statement to false + 'for (var i = 0; i < 10; i++) { };if(true); switch (x) {case 0: 2}', // mutates if statement to true + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0:}', // mutates switch statement + ); + }); + + it('should only mutate while, while do and boolean expression', () => { + expectJSMutationWithLevel( + sut, + conditionLevel2, + 'while (a > b) { }; do { } while (a > b); var x = a > b ? 1 : 2', + 'while (false) { }; do { } while (a > b); var x = a > b ? 1 : 2', // mutates while loop + 'while (a > b) { }; do { } while (a > b); var x = false ? 1 : 2', // mutates boolean to false + 'while (a > b) { }; do { } while (false); var x = a > b ? 1 : 2', // mutates while do loop + 'while (a > b) { }; do { } while (a > b); var x = true ? 1 : 2', // mutates boolean to false + ); + }); + + it('should only mutate all', () => { + expectJSMutationWithLevel( + sut, + conditionLevel3, + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = a > b ? 1 : 2', + 'for (var i = 0; false; i++) { };if(x > 2); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = a > b ? 1 : 2', // mutates for loop + 'for (var i = 0; i < 10; i++) { };if(false); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = a > b ? 1 : 2', // mutates if statement to false + 'for (var i = 0; i < 10; i++) { };if(true); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = a > b ? 1 : 2', // mutates if statement to true + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0:}; while (a > b); { } do { } while (a > b); var x = a > b ? 1 : 2', // mutates switch statement + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}; while (false); { } do { } while (a > b); var x = a > b ? 1 : 2', // mutates while loop + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = false ? 1 : 2', // mutates boolean to false + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}; while (a > b); { } do { } while (false); var x = a > b ? 1 : 2', // mutates while do loop + 'for (var i = 0; i < 10; i++) { };if(x > 2); switch (x) {case 0: 2}; while (a > b); { } do { } while (a > b); var x = true ? 1 : 2', // mutates boolean to false + ); + }); });