Skip to content

Commit

Permalink
#20 assignment operator (#46)
Browse files Browse the repository at this point in the history
* WIP support for assignmentoperator

* Added assignmentoperator in definitions

* Added tests for edge cases

* Added comments about what's being mutated

* Resolved merge conflict?
  • Loading branch information
Luctia authored and Ja4pp committed Jan 14, 2024
1 parent 21f2ba8 commit fbefe1e
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 19 deletions.
73 changes: 73 additions & 0 deletions packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@
"ArrayDeclaration": {
"$ref": "#/definitions/ArrayDeclaration"
},
"AssignmentOperator": {
"$ref": "#/definitions/AssignmentOperator"
},
"BlockStatement": {
"$ref": "#/definitions/BlockStatement"
},
Expand Down Expand Up @@ -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.",
Expand Down
47 changes: 30 additions & 17 deletions packages/instrumenter/src/mutators/assignment-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -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']);
Expand All @@ -25,21 +25,34 @@ 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;
}
},
};

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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"', () => {
Expand Down Expand Up @@ -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 ||=
);
});
});
2 changes: 1 addition & 1 deletion testing-project/stryker.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"name": "default",
"ArithmeticOperator": ["+To-", "-To+", "*To/"],
"ArrayDeclaration": ["EmptyArray", "FilledArray", "FilledArrayConstructor"],
"AssignmentOperator": ["-=To+=", "<<=To>>=", "&&=To||="],
"BooleanLiteral": ["TrueToFalse", "RemoveNegation"]

},
{
"name": "fancy",
Expand Down

0 comments on commit fbefe1e

Please sign in to comment.