diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 8daebda4dc..8b241b13d5 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -271,6 +271,9 @@ "ArrayDeclaration": { "$ref": "#/definitions/ArrayDeclaration" }, + "AssignmentOperator": { + "$ref": "#/definitions/AssignmentOperator" + }, "BlockStatement": { "$ref": "#/definitions/BlockStatement" }, @@ -374,6 +377,76 @@ ] } }, + "AssignmentOperator": { + "title": "AssignmentOperator", + "type": "array", + "uniqueItems": true, + "default": [], + "items": { + "anyOf": [ + { + "const" : "+=To-=", + "title": "PlusAssignmentToMinusAssignmentMutator", + "description": "Replace ```a += b``` with ```a -= b```." + }, + { + "const" : "-=To+=", + "title": "MinusAssignmentToPlusAssignmentMutator", + "description": "Replace ```a -= b``` with ```a += b```." + }, + { + "const" : "*=To/=", + "title": "MultiplyAssignmentToDivideAssignmentMutator", + "description": "Replace ```a *= b``` with ```a /= b```." + }, + { + "const" : "/=To*=", + "title": "DivideAssignmentToMultiplyAssignmentMutator", + "description": "Replace ```a /= b``` with ```a *= b```." + }, + { + "const" : "%=To*=", + "title": "ModuloAssignmentToMultiplyAssignmentMutator", + "description": "Replace ```a %= b``` with ```a *= b```." + }, + { + "const" : "<<=To>>=", + "title": "LeftShiftAssignmentToRightShiftAssignmentMutator", + "description": "Replace ```a <<= b``` with ```a >>= b```." + }, + { + "const" : ">>=To<<=", + "title": "RightShiftAssignmentToLeftShiftAssignmentMutator", + "description": "Replace ```a >>= b``` with ```a <<= b```." + }, + { + "const" : "&=To|=", + "title": "BitAndAssignmentToBitOrAssignmentMutator", + "description": "Replace ```a &= b``` with ```a |= b```." + }, + { + "const" : "|=To&=", + "title": "BitOrAssignmentToBitAndAssignmentMutator", + "description": "Replace ```a |= b``` with ```a &= b```." + }, + { + "const" : "&&=To||=", + "title": "LogicalAndAssignmentToLogicalOrAssignmentMutator", + "description": "Replace ```a &&= b``` with ```a ||= b```." + }, + { + "const" : "||=To&&=", + "title": "LogicalOrAssignmentToLogicalAndAssignmentMutator", + "description": "Replace ```a ||= b``` with ```a &&= b```." + }, + { + "const" : "??=To&&=", + "title": "NullishCoalescingAssignmentToLogicalAndAssignmentMutator", + "description": "Replace ```a ??= b``` with ```a &&= b```." + } + ] + } + }, "BlockStatement": { "title": "BlockStatementMutator", "description": "Removes the content of every block statement.", @@ -681,9 +754,14 @@ "description": "Replace ```\"\"``` with ```\"Stryker was here!\"```." }, { - "const": "Interpolation", - "title": "InterpolationMutator", + "const": "EmptyInterpolation", + "title": "EmptyInterpolation", "description": "Replace ```s\"foo ${bar}\"``` with ```s\"\"```." + }, + { + "const": "FillInterpolation", + "title": "FillInterpolation", + "description": "Replace ```s\"\"``` with ```s\"Stryker was here!\"```." } ] } diff --git a/packages/instrumenter/src/mutators/assignment-operator-mutator.ts b/packages/instrumenter/src/mutators/assignment-operator-mutator.ts index 3f17374ca0..b21612767b 100644 --- a/packages/instrumenter/src/mutators/assignment-operator-mutator.ts +++ b/packages/instrumenter/src/mutators/assignment-operator-mutator.ts @@ -1,22 +1,22 @@ -import type { types as t } from '@babel/core'; +import type { types } from '@babel/core'; import { deepCloneNode } from '../util/index.js'; import { NodeMutator } from './index.js'; const assignmentOperatorReplacements = Object.freeze({ - '+=': '-=', - '-=': '+=', - '*=': '/=', - '/=': '*=', - '%=': '*=', - '<<=': '>>=', - '>>=': '<<=', - '&=': '|=', - '|=': '&=', - '&&=': '||=', - '||=': '&&=', - '??=': '&&=', + '+=': { replacement: '-=', mutatorName: '+=To-=' }, + '-=': { replacement: '+=', mutatorName: '-=To+=' }, + '*=': { replacement: '/=', mutatorName: '*=To/=' }, + '/=': { replacement: '*=', mutatorName: '/=To*=' }, + '%=': { replacement: '*=', mutatorName: '%=To*=' }, + '<<=': { replacement: '>>=', mutatorName: '<<=To>>=' }, + '>>=': { replacement: '<<=', mutatorName: '>>=To<<=' }, + '&=': { replacement: '|=', mutatorName: '&=To|=' }, + '|=': { replacement: '&=', mutatorName: '|=To&=' }, + '&&=': { replacement: '||=', mutatorName: '&&=To||=' }, + '||=': { replacement: '&&=', mutatorName: '||=To&&=' }, + '??=': { replacement: '&&=', mutatorName: '??=To&&=' }, } as const); const stringTypes = Object.freeze(['StringLiteral', 'TemplateLiteral']); @@ -25,9 +25,14 @@ const stringAssignmentTypes = Object.freeze(['&&=', '||=', '??=']); export const assignmentOperatorMutator: NodeMutator = { name: 'AssignmentOperator', - *mutate(path) { - if (path.isAssignmentExpression() && isSupportedAssignmentOperator(path.node.operator) && isSupported(path.node)) { - const mutatedOperator = assignmentOperatorReplacements[path.node.operator]; + *mutate(path, options) { + if ( + path.isAssignmentExpression() && + isSupportedAssignmentOperator(path.node.operator) && + isSupported(path.node) && + isInMutationLevel(path.node, options) + ) { + const mutatedOperator = assignmentOperatorReplacements[path.node.operator].replacement; const replacement = deepCloneNode(path.node); replacement.operator = mutatedOperator; yield replacement; @@ -35,11 +40,19 @@ export const assignmentOperatorMutator: NodeMutator = { }, }; +function isInMutationLevel(node: types.AssignmentExpression, operations: string[] | undefined): boolean { + if (operations === undefined) { + return true; + } + const { mutatorName } = assignmentOperatorReplacements[node.operator as keyof typeof assignmentOperatorReplacements]; + return operations.some((op) => op === mutatorName); +} + function isSupportedAssignmentOperator(operator: string): operator is keyof typeof assignmentOperatorReplacements { return Object.keys(assignmentOperatorReplacements).includes(operator); } -function isSupported(node: t.AssignmentExpression): boolean { +function isSupported(node: types.AssignmentExpression): boolean { // Excludes assignment operators that apply to strings. if (stringTypes.includes(node.right.type) && !stringAssignmentTypes.includes(node.operator)) { return false; diff --git a/packages/instrumenter/src/mutators/boolean-literal-mutator.ts b/packages/instrumenter/src/mutators/boolean-literal-mutator.ts index c339c99b3d..97f1cd7977 100644 --- a/packages/instrumenter/src/mutators/boolean-literal-mutator.ts +++ b/packages/instrumenter/src/mutators/boolean-literal-mutator.ts @@ -6,15 +6,41 @@ const { types } = babel; import { NodeMutator } from './index.js'; +const booleanLiteralReplacements = Object.freeze({ + // prettier-ignore + 'true': {replacement: 'false', mutatorName: 'TrueToFalse'}, + // prettier-ignore + 'false': {replacement: 'true', mutatorName: 'FalseToTrue'}, + '!': { replacement: '', mutatorName: 'RemoveNegation' }, +} as const); + export const booleanLiteralMutator: NodeMutator = { name: 'BooleanLiteral', - *mutate(path) { - if (path.isBooleanLiteral()) { - yield types.booleanLiteral(!path.node.value); - } - if (path.isUnaryExpression() && path.node.operator === '!' && path.node.prefix) { - yield deepCloneNode(path.node.argument); + *mutate(path, options: string[] | undefined) { + if (isInMutationLevel(path, options)) { + if (path.isBooleanLiteral()) { + yield types.booleanLiteral(!path.node.value); + } + if (path.isUnaryExpression() && path.node.operator === '!' && path.node.prefix) { + yield deepCloneNode(path.node.argument); + } } }, }; + +function isInMutationLevel(path: any, mutators: string[] | undefined): boolean { + if (mutators === undefined) { + return true; + } + if (path.isBooleanLiteral()) { + const { mutatorName } = booleanLiteralReplacements[path.node.value as keyof typeof booleanLiteralReplacements]; + return mutators.some((lit) => lit === mutatorName); + } + return ( + path.isUnaryExpression() && + path.node.operator === '!' && + path.node.prefix && + mutators.some((lit: string) => lit === booleanLiteralReplacements['!'].mutatorName) + ); +} diff --git a/packages/instrumenter/src/mutators/mutation-level-options.ts b/packages/instrumenter/src/mutators/mutation-level-options.ts index 8da2c93238..09dfaf59c2 100644 --- a/packages/instrumenter/src/mutators/mutation-level-options.ts +++ b/packages/instrumenter/src/mutators/mutation-level-options.ts @@ -3,3 +3,4 @@ import { MutationLevel } from '@stryker-mutator/api/core'; export interface RunLevelOptions { runLevel?: MutationLevel; } +export type MutationOperator = Record; diff --git a/packages/instrumenter/src/mutators/string-literal-mutator.ts b/packages/instrumenter/src/mutators/string-literal-mutator.ts index 9b3dc4c03f..2b66792a1d 100644 --- a/packages/instrumenter/src/mutators/string-literal-mutator.ts +++ b/packages/instrumenter/src/mutators/string-literal-mutator.ts @@ -1,19 +1,43 @@ import babel, { type NodePath } from '@babel/core'; import { NodeMutator } from './node-mutator.js'; +import { MutationOperator } from './mutation-level-options.js'; const { types } = babel; +const operators: MutationOperator = { + FillString: { replacementOperator: types.stringLiteral('Stryker was here!'), mutatorName: 'FillString' }, + EmptyString: { replacementOperator: types.stringLiteral(''), mutatorName: 'EmptyString' }, + EmptyInterpolation: { replacementOperator: types.templateLiteral([types.templateElement({ raw: '' })], []), mutatorName: 'EmptyInterpolation' }, + FillInterpolation: { + replacementOperator: types.templateLiteral([types.templateElement({ raw: 'Stryker was here!' })], []), + mutatorName: 'FillInterpolation', + }, +}; + export const stringLiteralMutator: NodeMutator = { name: 'StringLiteral', - *mutate(path) { + *mutate(path, operations: string[] | undefined) { if (path.isTemplateLiteral()) { - const replacement = path.node.quasis.length === 1 && path.node.quasis[0].value.raw.length === 0 ? 'Stryker was here!' : ''; - yield types.templateLiteral([types.templateElement({ raw: replacement })], []); + const stringIsEmpty = path.node.quasis.length === 1 && path.node.quasis[0].value.raw.length === 0; + if ( + operations === undefined || + (stringIsEmpty && operations.includes(operators.FillInterpolation.mutatorName)) || + (!stringIsEmpty && operations.includes(operators.EmptyInterpolation.mutatorName)) + ) { + yield stringIsEmpty ? operators.FillInterpolation.replacementOperator : operators.EmptyInterpolation.replacementOperator; + } } if (path.isStringLiteral() && isValidParent(path)) { - yield types.stringLiteral(path.node.value.length === 0 ? 'Stryker was here!' : ''); + const stringIsEmpty = path.node.value.length === 0; + if ( + operations === undefined || + (stringIsEmpty && operations.includes(operators.FillString.mutatorName)) || + (!stringIsEmpty && operations.includes(operators.EmptyString.mutatorName)) + ) { + yield stringIsEmpty ? operators.FillString.replacementOperator : operators.EmptyString.replacementOperator; + } } }, }; diff --git a/packages/instrumenter/test/unit/mutators/assignment-operator-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/assignment-operator-mutator.spec.ts index 8fe2a137b0..d86d5a406c 100644 --- a/packages/instrumenter/test/unit/mutators/assignment-operator-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/assignment-operator-mutator.spec.ts @@ -1,7 +1,29 @@ import { expect } from 'chai'; +import { MutationLevel } from '@stryker-mutator/api/core'; + import { assignmentOperatorMutator as sut } from '../../../src/mutators/assignment-operator-mutator.js'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; + +const assignmentOperatorLevel: MutationLevel = { name: 'AssignmentOperatorLevel', AssignmentOperator: ['-=To+=', '<<=To>>=', '&&=To||='] }; +const assignmentOperatorAllLevel: MutationLevel = { + name: 'AssignmentOperatorLevel', + AssignmentOperator: [ + '+=To-=', + '-=To+=', + '*=To/=', + '/=To*=', + '%=To*=', + '<<=To>>=', + '>>=To<<=', + '&=To|=', + '|=To&=', + '&&=To||=', + '||=To&&=', + '??=To&&=', + ], +}; +const assignmentOperatorUndefinedLevel: MutationLevel = { name: 'AssignmentOperatorLevel' }; describe(sut.name, () => { it('should have name "AssignmentOperator"', () => { @@ -72,4 +94,47 @@ describe(sut.name, () => { expectJSMutation(sut, 'a ||= `b`', 'a &&= `b`'); expectJSMutation(sut, 'a ??= `b`', 'a &&= `b`'); }); + + it('should only mutate what is defined in the mutator level', () => { + expectJSMutationWithLevel( + sut, + assignmentOperatorLevel.AssignmentOperator, + 'a += b; a -= b; a *= b; a /= b; a <<= b; a &&= b;', + 'a += b; a += b; a *= b; a /= b; a <<= b; a &&= b;', // mutated -= to += + 'a += b; a -= b; a *= b; a /= b; a >>= b; a &&= b;', // mutated <<= to >>= + 'a += b; a -= b; a *= b; a /= b; a <<= b; a ||= b;', // mutated &&= to ||= + ); + }); + + it('should not mutate anything if there are no values in the mutation level', () => { + expectJSMutationWithLevel(sut, [], 'a += b; a -= b; a *= b; a /= b; a <<= b; a &&= b;'); + }); + + it('should mutate everything if everything is in the mutation level', () => { + expectJSMutationWithLevel( + sut, + assignmentOperatorAllLevel.BooleanLiteral, + 'a += b; a -= b; a *= b; a /= b; a <<= b; a &&= b;', + 'a -= b; a -= b; a *= b; a /= b; a <<= b; a &&= b;', // mutated += to -= + 'a += b; a += b; a *= b; a /= b; a <<= b; a &&= b;', // mutated -= to += + 'a += b; a -= b; a /= b; a /= b; a <<= b; a &&= b;', // mutated *= to /= + 'a += b; a -= b; a *= b; a *= b; a <<= b; a &&= b;', // mutated /= to *= + 'a += b; a -= b; a *= b; a /= b; a >>= b; a &&= b;', // mutated <<= to >>= + 'a += b; a -= b; a *= b; a /= b; a <<= b; a ||= b;', // mutated &&= to ||= + ); + }); + + it('should mutate everything if the mutation level is undefined', () => { + expectJSMutationWithLevel( + sut, + assignmentOperatorUndefinedLevel.BooleanLiteral, + 'a += b; a -= b; a *= b; a /= b; a <<= b; a &&= b;', + 'a -= b; a -= b; a *= b; a /= b; a <<= b; a &&= b;', // mutated += to -= + 'a += b; a += b; a *= b; a /= b; a <<= b; a &&= b;', // mutated -= to += + 'a += b; a -= b; a /= b; a /= b; a <<= b; a &&= b;', // mutated *= to /= + 'a += b; a -= b; a *= b; a *= b; a <<= b; a &&= b;', // mutated /= to *= + 'a += b; a -= b; a *= b; a /= b; a >>= b; a &&= b;', // mutated <<= to >>= + 'a += b; a -= b; a *= b; a /= b; a <<= b; a ||= b;', // mutated &&= to ||= + ); + }); }); diff --git a/packages/instrumenter/test/unit/mutators/boolean-literal-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/boolean-literal-mutator.spec.ts index 1cf171553e..23ee6ee178 100644 --- a/packages/instrumenter/test/unit/mutators/boolean-literal-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/boolean-literal-mutator.spec.ts @@ -1,7 +1,23 @@ import { expect } from 'chai'; +import { MutationLevel } from '@stryker-mutator/api/core'; + import { booleanLiteralMutator as sut } from '../../../src/mutators/boolean-literal-mutator.js'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; + +const booleanLiteralLevel: MutationLevel = { + name: 'BooleanLiteralLevel', + BooleanLiteral: ['TrueToFalse', 'RemoveNegation'], +}; + +const booleanLiteralAllLevel: MutationLevel = { + name: 'BooleanLiteralLevel', + BooleanLiteral: ['TrueToFalse', 'FalseToTrue', 'RemoveNegation'], +}; + +const booleanLiteralUndefinedLevel: MutationLevel = { + name: 'BooleanLiteralLevel', +}; describe(sut.name, () => { it('should have name "BooleanLiteral"', () => { @@ -19,4 +35,40 @@ describe(sut.name, () => { it('should mutate !a to a', () => { expectJSMutation(sut, '!a', 'a'); }); + + it('should only mutate what is defined in the mutation level', () => { + expectJSMutationWithLevel( + sut, + booleanLiteralLevel.BooleanLiteral, + 'if (true) {}; if (false) {}; if (!value) {}', + 'if (false) {}; if (false) {}; if (!value) {}', + 'if (true) {}; if (false) {}; if (value) {}', + ); + }); + + it('should not mutate anything if there are no values in the mutation level', () => { + expectJSMutationWithLevel(sut, [], 'if (true) {}; if (false) {}; if (!value) {}'); + }); + + it('should mutate everything if everything is in the mutation level', () => { + expectJSMutationWithLevel( + sut, + booleanLiteralAllLevel.BooleanLiteral, + 'if (true) {}; if (false) {}; if (!value) {}', + 'if (false) {}; if (false) {}; if (!value) {}', + 'if (true) {}; if (false) {}; if (value) {}', + 'if (true) {}; if (true) {}; if (!value) {}', + ); + }); + + it('should mutate everything if the mutation level is undefined', () => { + expectJSMutationWithLevel( + sut, + booleanLiteralUndefinedLevel.BooleanLiteral, + 'if (true) {}; if (false) {}; if (!value) {}', + 'if (false) {}; if (false) {}; if (!value) {}', + 'if (true) {}; if (false) {}; if (value) {}', + 'if (true) {}; if (true) {}; if (!value) {}', + ); + }); }); diff --git a/packages/instrumenter/test/unit/mutators/string-literal-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/string-literal-mutator.spec.ts index fd12ec05e3..af720fc834 100644 --- a/packages/instrumenter/test/unit/mutators/string-literal-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/string-literal-mutator.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; import { stringLiteralMutator as sut } from '../../../src/mutators/string-literal-mutator.js'; describe(sut.name, () => { @@ -112,4 +112,30 @@ describe(sut.name, () => { expectJSMutation(sut, ''); }); }); + + describe('mutation level', () => { + it('should only mutate EmptyString and EmptyInterpolation from all possible mutations', () => { + expectJSMutationWithLevel( + sut, + ['EmptyString', 'EmptyInterpolation'], + 'const bar = "bar"; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=``', + 'const bar = ""; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=``', // empties string + 'const bar = "bar"; const foo = ``; const emptyString=""; const emptyInterp=``', // empties interpolation + ); + }); + it('should block the mutators', () => { + expectJSMutationWithLevel(sut, [], 'const bar = "bar"; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=``'); + }); + it('should mutate everything', () => { + expectJSMutationWithLevel( + sut, + undefined, + 'const bar = "bar"; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=``', + 'const bar = ""; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=``', // empties string literal + 'const bar = "bar"; const foo = ``; const emptyString=""; const emptyInterp=``', // empties interpolation + 'const bar = "bar"; const foo = `name: ${level_name}`; const emptyString="Stryker was here!"; const emptyInterp=``', // fills string literal + 'const bar = "bar"; const foo = `name: ${level_name}`; const emptyString=""; const emptyInterp=`Stryker was here!`', // fills interpolation + ); + }); + }); }); diff --git a/testing-project/stryker.conf.json b/testing-project/stryker.conf.json index 86f14b22a4..abf4dda2f9 100644 --- a/testing-project/stryker.conf.json +++ b/testing-project/stryker.conf.json @@ -18,7 +18,12 @@ "mutationLevels": [ { "name": "default", - "UpdateOperator": ["Pre--To++", "Pre++To--", "Post++To--", "Post--To++"] + "ArithmeticOperator": ["+To-", "-To+", "*To/"], + "ArrayDeclaration": ["EmptyArray", "FilledArray", "FilledArrayConstructor"], + "StringLiteral": ["EmptyString", "FillString", "EmptyInterpolation", "FillInterpolation"], + "AssignmentOperator": ["-=To+=", "<<=To>>=", "&&=To||="], + "BooleanLiteral": ["TrueToFalse", "RemoveNegation"] + }, { "name": "fancy",