Skip to content

Commit

Permalink
#23 restrict equalityoperator mutator (#41)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Ivo_Broekhof <[email protected]>
  • Loading branch information
3 people authored Nov 22, 2023
1 parent 9ec5d92 commit f9fbcf6
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 23 deletions.
13 changes: 12 additions & 1 deletion packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
"title": "MutationLevel",
"type": "object",
"default": {},
"additionalProperties": false,
"required": ["name"],
"properties": {
"name": {
"description": "Name of the mutation level.",
Expand Down Expand Up @@ -310,6 +310,7 @@
"title": "ArithmeticOperator",
"type": "array",
"uniqueItems": true,
"minItems": 1,
"default": [],
"items": {
"anyOf": [
Expand Down Expand Up @@ -347,6 +348,7 @@
"description": "EmptyArray := \nEmptyArrayConstructor := \nFilledArray := \nFilledArrayConstructor := ",
"uniqueItems": true,
"default": [],
"minItems": 1,
"items": {
"anyOf": [
{
Expand Down Expand Up @@ -383,6 +385,7 @@
"type": "array",
"uniqueItems": true,
"default": [],
"minItems": 1,
"items": {
"anyOf": [
{
Expand All @@ -408,6 +411,7 @@
"type": "array",
"default": [],
"uniqueItems": true,
"minItems": 1,
"items": {
"anyOf": [
{
Expand Down Expand Up @@ -453,6 +457,7 @@
"type": "array",
"uniqueItems": true,
"default": [],
"minItems": 1,
"items": {
"anyOf": [
{
Expand Down Expand Up @@ -522,6 +527,7 @@
"title": "LogicalOperator",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand All @@ -547,6 +553,7 @@
"title": "MethodExpression",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand Down Expand Up @@ -633,6 +640,7 @@
"title": "OptionalChaining",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand All @@ -658,6 +666,7 @@
"title": "StringLiteral",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand All @@ -683,6 +692,7 @@
"title": "UnaryOperator",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand All @@ -708,6 +718,7 @@
"title": "UpdateOperator",
"type": "array",
"default": [],
"minItems": 1,
"uniqueItems": true,
"items": {
"anyOf": [
Expand Down
26 changes: 18 additions & 8 deletions packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,36 @@ 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;
}
},
};

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;
Expand Down
49 changes: 37 additions & 12 deletions packages/instrumenter/src/mutators/equality-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -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: '<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;

function isEqualityOperator(operator: string): operator is keyof typeof operators {
Expand All @@ -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));
}
Original file line number Diff line number Diff line change
@@ -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"', () => {
Expand Down Expand Up @@ -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 *
);
});
});
Original file line number Diff line number Diff line change
@@ -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>', '>=To<', '==To!='] };

const equalityLevelB: MutationLevel = { name: 'EqualityLevelB', EqualityOperator: ['<=To>', '>To<=', '===To!=='] };

describe(sut.name, () => {
it('should have name "EqualityOperator"', () => {
Expand All @@ -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 !==
);
});
});
1 change: 1 addition & 0 deletions testing-project/stryker.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"mutationLevels": [
{
"name": "default",
"ArithmeticOperator": ["+To-", "-To+", "*To/"],
"ArrayDeclaration": ["EmptyArray", "FilledArray", "FilledArrayConstructor"]

},
Expand Down

0 comments on commit f9fbcf6

Please sign in to comment.