From 73fb957345be824cfdc2af741433bb920222d65e Mon Sep 17 00:00:00 2001 From: Ivo Broekhof <46261484+brokhiv@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:13:49 +0100 Subject: [PATCH] #23 restrict equalityoperator mutator (#41) * ArithmeticOp: pass the whole level * Add min size to level arrays and mandatory name * Move arithmetic ops to map * Add test for arithmetic operator * Restrict EqualityOperator Observed mutation score (equality-operator-mutator.ts): 85.71% * Formatting fixes * Formatting fixes and removed dead code * Update arithmetic-operator-mutator.ts and equality-operator-mutator.ts to comply with the changes to node-mutator.ts. Coded it such that `undefined` results in allowing everything since otherwise setting no MutationLevel results in blocking every mutator. --------- Co-authored-by: Danut Copae Co-authored-by: Ivo_Broekhof --- packages/api/schema/stryker-core.json | 13 ++++- .../mutators/arithmetic-operator-mutator.ts | 26 +++++++--- .../src/mutators/equality-operator-mutator.ts | 49 ++++++++++++++----- .../arithmatic-operator-mutator.spec.ts | 17 ++++++- .../equality-operator-mutator.spec.ts | 32 +++++++++++- testing-project/stryker.conf.json | 1 + 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 1e8bfbbb42..8daebda4dc 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -258,7 +258,7 @@ "title": "MutationLevel", "type": "object", "default": {}, - "additionalProperties": false, + "required": ["name"], "properties": { "name": { "description": "Name of the mutation level.", @@ -310,6 +310,7 @@ "title": "ArithmeticOperator", "type": "array", "uniqueItems": true, + "minItems": 1, "default": [], "items": { "anyOf": [ @@ -347,6 +348,7 @@ "description": "EmptyArray := \nEmptyArrayConstructor := \nFilledArray := \nFilledArrayConstructor := ", "uniqueItems": true, "default": [], + "minItems": 1, "items": { "anyOf": [ { @@ -383,6 +385,7 @@ "type": "array", "uniqueItems": true, "default": [], + "minItems": 1, "items": { "anyOf": [ { @@ -408,6 +411,7 @@ "type": "array", "default": [], "uniqueItems": true, + "minItems": 1, "items": { "anyOf": [ { @@ -453,6 +457,7 @@ "type": "array", "uniqueItems": true, "default": [], + "minItems": 1, "items": { "anyOf": [ { @@ -522,6 +527,7 @@ "title": "LogicalOperator", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ @@ -547,6 +553,7 @@ "title": "MethodExpression", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ @@ -633,6 +640,7 @@ "title": "OptionalChaining", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ @@ -658,6 +666,7 @@ "title": "StringLiteral", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ @@ -683,6 +692,7 @@ "title": "UnaryOperator", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ @@ -708,6 +718,7 @@ "title": "UpdateOperator", "type": "array", "default": [], + "minItems": 1, "uniqueItems": true, "items": { "anyOf": [ diff --git a/packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts b/packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts index 18c21e58fc..fe940c42bf 100644 --- a/packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts +++ b/packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts @@ -5,19 +5,19 @@ import { deepCloneNode } from '../util/index.js'; import { NodeMutator } from './node-mutator.js'; const arithmeticOperatorReplacements = Object.freeze({ - '+': '-', - '-': '+', - '*': '/', - '/': '*', - '%': '*', + '+': { replacement: '-', mutatorName: '+To-' }, + '-': { replacement: '+', mutatorName: '-To+' }, + '*': { replacement: '/', mutatorName: '*To/' }, + '/': { replacement: '*', mutatorName: '/To*' }, + '%': { replacement: '*', mutatorName: '%To*' }, } as const); export const arithmeticOperatorMutator: NodeMutator = { name: 'ArithmeticOperator', - *mutate(path) { - if (path.isBinaryExpression() && isSupported(path.node.operator, path.node)) { - const mutatedOperator = arithmeticOperatorReplacements[path.node.operator]; + *mutate(path, options) { + if (path.isBinaryExpression() && isSupported(path.node.operator, path.node) && isInMutationLevel(path.node, options)) { + const mutatedOperator = arithmeticOperatorReplacements[path.node.operator].replacement; const replacement = deepCloneNode(path.node); replacement.operator = mutatedOperator; yield replacement; @@ -25,6 +25,16 @@ export const arithmeticOperatorMutator: NodeMutator = { }, }; +function isInMutationLevel(node: types.BinaryExpression, operations: string[] | undefined): boolean { + // No mutation level specified, so allow everything + if (operations === undefined) { + return true; + } + + const mutatedOperator = arithmeticOperatorReplacements[node.operator as keyof typeof arithmeticOperatorReplacements].mutatorName; + return operations.some((op) => op === mutatedOperator) ?? false; +} + function isSupported(operator: string, node: types.BinaryExpression): operator is keyof typeof arithmeticOperatorReplacements { if (!Object.keys(arithmeticOperatorReplacements).includes(operator)) { return false; diff --git a/packages/instrumenter/src/mutators/equality-operator-mutator.ts b/packages/instrumenter/src/mutators/equality-operator-mutator.ts index 0aace5eb0d..b8c1fa23e6 100644 --- a/packages/instrumenter/src/mutators/equality-operator-mutator.ts +++ b/packages/instrumenter/src/mutators/equality-operator-mutator.ts @@ -1,18 +1,30 @@ -import babel from '@babel/core'; +import babel, { types } from '@babel/core'; import { NodeMutator } from './node-mutator.js'; const { types: t } = babel; const operators = { - '<': ['<=', '>='], - '<=': ['<', '>'], - '>': ['>=', '<='], - '>=': ['>', '<'], - '==': ['!='], - '!=': ['=='], - '===': ['!=='], - '!==': ['==='], + '<': [ + { replacement: '<=', mutatorName: '=', mutatorName: '=' }, + ], + '<=': [ + { 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; function isEqualityOperator(operator: string): operator is keyof typeof operators { @@ -21,13 +33,26 @@ function isEqualityOperator(operator: string): operator is keyof typeof operator export const equalityOperatorMutator: NodeMutator = { name: 'EqualityOperator', - *mutate(path) { + *mutate(path, operations) { if (path.isBinaryExpression() && isEqualityOperator(path.node.operator)) { - for (const mutableOperator of operators[path.node.operator]) { + const allMutations = filterMutationLevel(path.node, operations); + // throw new Error(allMutations.toString()); + for (const mutableOperator of allMutations) { const replacement = t.cloneNode(path.node, true); - replacement.operator = mutableOperator; + replacement.operator = mutableOperator.replacement; yield replacement; } } }, }; + +function filterMutationLevel(node: types.BinaryExpression, operations: string[] | undefined) { + const allMutations = operators[node.operator as keyof typeof operators]; + + // Nothing allowed, so return an empty array + if (operations === undefined) { + return allMutations; + } + + return allMutations.filter((mut) => operations.some((op) => op === mut.mutatorName)); +} diff --git a/packages/instrumenter/test/unit/mutators/arithmatic-operator-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/arithmatic-operator-mutator.spec.ts index ecda7f8d17..0bcedc294c 100644 --- a/packages/instrumenter/test/unit/mutators/arithmatic-operator-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/arithmatic-operator-mutator.spec.ts @@ -1,7 +1,11 @@ import { expect } from 'chai'; +import { MutationLevel } from '@stryker-mutator/api/core'; + import { arithmeticOperatorMutator as sut } from '../../../src/mutators/arithmetic-operator-mutator.js'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; + +const arithmeticLevel: MutationLevel = { name: 'ArithemticLevel', ArithmeticOperator: ['+To-', '-To+', '*To/'] }; describe(sut.name, () => { it('should have name "ArithmeticOperator"', () => { @@ -30,4 +34,15 @@ describe(sut.name, () => { expectJSMutation(sut, '"a" + b + "c" + d + "e"'); }); + + it('should only mutate +, - and * from all possible mutators', () => { + expectJSMutationWithLevel( + sut, + arithmeticLevel.ArithmeticOperator, + 'a + b; a - b; a * b; a % b; a / b; a % b', + 'a - b; a - b; a * b; a % b; a / b; a % b', // mutates + + 'a + b; a + b; a * b; a % b; a / b; a % b', // mutates - + 'a + b; a - b; a / b; a % b; a / b; a % b', // mutates * + ); + }); }); diff --git a/packages/instrumenter/test/unit/mutators/equality-operator-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/equality-operator-mutator.spec.ts index f62e11f988..1c86ed9108 100644 --- a/packages/instrumenter/test/unit/mutators/equality-operator-mutator.spec.ts +++ b/packages/instrumenter/test/unit/mutators/equality-operator-mutator.spec.ts @@ -1,7 +1,13 @@ import { expect } from 'chai'; +import { MutationLevel } from '@stryker-mutator/api/core'; + import { equalityOperatorMutator as sut } from '../../../src/mutators/equality-operator-mutator.js'; -import { expectJSMutation } from '../../helpers/expect-mutation.js'; +import { expectJSMutation, expectJSMutationWithLevel } from '../../helpers/expect-mutation.js'; + +const equalityLevelA: MutationLevel = { name: 'EqualityLevelA', EqualityOperator: ['=', '>=To>', '>=To<', '==To!='] }; + +const equalityLevelB: MutationLevel = { name: 'EqualityLevelB', EqualityOperator: ['<=To>', '>To<=', '===To!=='] }; describe(sut.name, () => { it('should have name "EqualityOperator"', () => { @@ -27,4 +33,28 @@ describe(sut.name, () => { expectJSMutation(sut, 'a != b', 'a == b'); expectJSMutation(sut, 'a !== b', 'a === b'); }); + + it('should only mutate <, >=, == from all possible mutators', () => { + expectJSMutationWithLevel( + sut, + equalityLevelA.EqualityOperator, + 'a < b; a <= b; a > b; a >= b; a == b; a != b; a === b; a !== b', + 'a <= b; a <= b; a > b; a >= b; a == b; a != b; a === b; a !== b', // mutates < + 'a >= b; a <= b; a > b; a >= b; a == b; a != b; a === b; a !== b', // mutates < + 'a < b; a <= b; a > b; a > b; a == b; a != b; a === b; a !== b', // mutates >= + 'a < b; a <= b; a > b; a < b; a == b; a != b; a === b; a !== b', // mutates >= + 'a < b; a <= b; a > b; a >= b; a != b; a != b; a === b; a !== b', // mutates == + ); + }); + + it('should only mutate <= to >, > to <=, and === to !== from all possible mutators', () => { + expectJSMutationWithLevel( + sut, + equalityLevelB.EqualityOperator, + 'a < b; a <= b; a > b; a >= b; a == b; a != b; a === b; a !== b', + 'a < b; a > b; a > b; a >= b; a == b; a != b; a === b; a !== b', // mutates <= to > + 'a < b; a <= b; a <= b; a >= b; a == b; a != b; a === b; a !== b', // mutates > to <= + 'a < b; a <= b; a > b; a >= b; a == b; a != b; a !== b; a !== b', // mutates === to !== + ); + }); }); diff --git a/testing-project/stryker.conf.json b/testing-project/stryker.conf.json index 87d4dd40ae..c955301f07 100644 --- a/testing-project/stryker.conf.json +++ b/testing-project/stryker.conf.json @@ -18,6 +18,7 @@ "mutationLevels": [ { "name": "default", + "ArithmeticOperator": ["+To-", "-To+", "*To/"], "ArrayDeclaration": ["EmptyArray", "FilledArray", "FilledArrayConstructor"] },