Skip to content

Commit

Permalink
Merge commit '4d8db03b89dc6312471d8c0d9cd56cb94629c950' into #30-rest…
Browse files Browse the repository at this point in the history
…rict-updateoperator-mutator
  • Loading branch information
Ja4pp committed Nov 26, 2023
2 parents 3f27326 + 4d8db03 commit 5f8bd1c
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 33 deletions.
82 changes: 80 additions & 2 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 Expand Up @@ -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!\"```."
}
]
}
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
38 changes: 32 additions & 6 deletions packages/instrumenter/src/mutators/boolean-literal-mutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ import { MutationLevel } from '@stryker-mutator/api/core';
export interface RunLevelOptions {
runLevel?: MutationLevel;
}
export type MutationOperator = Record<string, { replacementOperator: any; mutatorName: string }>;
32 changes: 28 additions & 4 deletions packages/instrumenter/src/mutators/string-literal-mutator.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
},
};
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 ||=
);
});
});
Loading

0 comments on commit 5f8bd1c

Please sign in to comment.