Skip to content

Commit

Permalink
feat(mutators): add mutators to instrumenter package (#2266)
Browse files Browse the repository at this point in the history
Add our existing mutators:

* ArithmeticOperator
* ArrayDeclaration
* ArrowFunction
* BlockStatement
* BooleanLiteral
* ConditionalExpression
* EqualityOperator
* LogicalOperator
* ObjectLiteral
* StringLiteral
* UnaryOperator
* UpdateOperator
  • Loading branch information
simondel authored Jun 19, 2020
1 parent 482c782 commit 3b87743
Show file tree
Hide file tree
Showing 29 changed files with 808 additions and 102 deletions.
4 changes: 3 additions & 1 deletion packages/instrumenter/src/mutant-placers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export function placeMutant(node: NodePath, mutants: Mutant[], mutantPlacers: re
}
} catch (error) {
throw new Error(
`Error while placing mutants on ${node.node.loc?.start.line}:${node.node.loc?.start.column} with ${placer.name}. ${error.stack}`
`Error while placing mutants of type(s) "${mutants.map((mutant) => mutant.mutatorName).join(', ')}" on ${node.node.loc?.start.line}:${
node.node.loc?.start.column
} with ${placer.name}. ${error.stack}`
);
}
}
Expand Down
28 changes: 2 additions & 26 deletions packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,9 @@ import { NodeMutation } from '../mutant';

import { NodeMutator } from './node-mutator';

type Operator =
| '+'
| '-'
| '/'
| '%'
| '*'
| '**'
| '&'
| '|'
| '>>'
| '>>>'
| '<<'
| '^'
| '=='
| '==='
| '!='
| '!=='
| 'in'
| 'instanceof'
| '>'
| '<'
| '>='
| '<=';

export class ArithmeticOperatorMutator implements NodeMutator {
private readonly operators: {
[op: string]: Operator | undefined;
[op: string]: BinaryOperator | undefined;
} = Object.freeze({
'+': '-',
'-': '+',
Expand All @@ -42,7 +18,7 @@ export class ArithmeticOperatorMutator implements NodeMutator {
public name = 'ArithmeticOperator';

public mutate(path: NodePath): NodeMutation[] {
if (types.isBinaryExpression(path.node)) {
if (path.isBinaryExpression()) {
const mutatedOperator = this.operators[path.node.operator];
if (mutatedOperator) {
const replacement = types.cloneNode(path.node, false);
Expand Down
24 changes: 24 additions & 0 deletions packages/instrumenter/src/mutators/array-declaration-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NodePath, types } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from './node-mutator';

export class ArrayDeclarationMutator implements NodeMutator {
public name = 'ArrayDeclaration';

public mutate(path: NodePath): NodeMutation[] {
if (path.isArrayExpression()) {
const replacement = path.node.elements.length ? types.arrayExpression() : types.arrayExpression([types.stringLiteral('Stryker was here')]);
return [{ original: path.node, replacement }];
} else if ((path.isCallExpression() || path.isNewExpression()) && types.isIdentifier(path.node.callee) && path.node.callee.name === 'Array') {
const mutatedCallArgs = path.node.arguments && path.node.arguments.length ? [] : [types.arrayExpression()];
const replacement = types.isNewExpression(path)
? types.newExpression(path.node.callee, mutatedCallArgs)
: types.callExpression(path.node.callee, mutatedCallArgs);
return [{ original: path.node, replacement }];
} else {
return [];
}
}
}
16 changes: 16 additions & 0 deletions packages/instrumenter/src/mutators/arrow-function-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class ArrowFunctionMutator implements NodeMutator {
public name = 'ArrowFunction';

public mutate(path: NodePath): NodeMutation[] {
return path.isArrowFunctionExpression() && !types.isBlockStatement(path.node.body)
? [{ original: path.node, replacement: types.arrowFunctionExpression([], types.identifier('undefined')) }]
: [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class BlockStatementMutator implements NodeMutator {
public name = 'BlockStatement';

public mutate(path: NodePath): NodeMutation[] {
if (path.isBlockStatement()) {
if (path.isBlockStatement() && path.node.body.length) {
const replacement = types.cloneNode(path.node, false);
replacement.body = [];
return [{ original: path.node, replacement }];
Expand Down
22 changes: 22 additions & 0 deletions packages/instrumenter/src/mutators/boolean-literal-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class BooleanLiteralMutator implements NodeMutator {
public name = 'BooleanLiteral';

private readonly unaryBooleanPrefix = '!';

public mutate(path: NodePath): NodeMutation[] {
if (path.isBooleanLiteral()) {
return [{ original: path.node, replacement: types.booleanLiteral(!path.node.value) }];
} else if (path.isUnaryExpression() && path.node.operator === this.unaryBooleanPrefix && path.node.prefix) {
return [{ original: path.node, replacement: types.cloneNode(path.node.argument, false) }];
}

return [];
}
}
39 changes: 39 additions & 0 deletions packages/instrumenter/src/mutators/equality-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { types, NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from './node-mutator';

export class EqualityOperatorMutator implements NodeMutator {
private readonly operators: { [targetedOperator: string]: BinaryOperator[] } = {
'<': ['<=', '>='],
'<=': ['<', '>'],
'>': ['>=', '<='],
'>=': ['>', '<'],
'==': ['!='],
'!=': ['=='],
'===': ['!=='],
'!==': ['==='],
};

public name = 'EqualityOperator';

public mutate(path: NodePath): NodeMutation[] {
if (path.isBinaryExpression()) {
let mutatedOperators = this.operators[path.node.operator];
if (mutatedOperators) {
return mutatedOperators.map((mutatedOperator) => {
const replacement = types.cloneNode(path.node, false) as types.BinaryExpression;
replacement.operator = mutatedOperator;

return {
original: path.node,
replacement,
};
});
}
}

return [];
}
}
16 changes: 16 additions & 0 deletions packages/instrumenter/src/mutators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,29 @@ import { NodeMutator } from './node-mutator';
import { BlockStatementMutator } from './block-statement-mutator';
import { ConditionalExpressionMutator } from './conditional-expression-mutator';
import { StringLiteralMutator } from './string-literal-mutator';
import { ArrayDeclarationMutator } from './array-declaration-mutator';
import { ArrowFunctionMutator } from './arrow-function-mutator';
import { BooleanLiteralMutator } from './boolean-literal-mutator';
import { EqualityOperatorMutator } from './equality-operator-mutator';
import { LogicalOperatorMutator } from './logical-operator-mutator';
import { ObjectLiteralMutator } from './object-literal-mutator';
import { UnaryOperatorMutator } from './unary-operator-mutator';
import { UpdateOperatorMutator } from './update-operator-mutator';

export * from './node-mutator';
export const mutators: NodeMutator[] = [
new ArithmeticOperatorMutator(),
new ArrayDeclarationMutator(),
new ArrowFunctionMutator(),
new BlockStatementMutator(),
new BooleanLiteralMutator(),
new ConditionalExpressionMutator(),
new EqualityOperatorMutator(),
new LogicalOperatorMutator(),
new ObjectLiteralMutator(),
new StringLiteralMutator(),
new UnaryOperatorMutator(),
new UpdateOperatorMutator(),
];
export const mutate = (node: NodePath): NamedNodeMutation[] => {
return flatMap(mutators, (mutator) => mutator.mutate(node).map((nodeMutation) => ({ ...nodeMutation, mutatorName: mutator.name })));
Expand Down
28 changes: 28 additions & 0 deletions packages/instrumenter/src/mutators/logical-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class LogicalOperatorMutator implements NodeMutator {
public name = 'LogicalOperator';

private readonly operators: { [targetedOperator: string]: '||' | '&&' } = {
'&&': '||',
'||': '&&',
};

public mutate(path: NodePath): NodeMutation[] {
if (path.isLogicalExpression()) {
const mutatedOperator = this.operators[path.node.operator];
if (mutatedOperator) {
const replacement = types.cloneNode(path.node, false);
replacement.operator = mutatedOperator;
return [{ original: path.node, replacement }];
}
}

return [];
}
}
14 changes: 14 additions & 0 deletions packages/instrumenter/src/mutators/object-literal-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class ObjectLiteralMutator implements NodeMutator {
public name = 'ObjectLiteral';

public mutate(path: NodePath): NodeMutation[] {
return path.isObjectExpression() && path.node.properties.length > 0 ? [{ original: path.node, replacement: types.objectExpression([]) }] : [];
}
}
14 changes: 11 additions & 3 deletions packages/instrumenter/src/mutators/string-literal-mutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class StringLiteralMutator implements NodeMutator {
},
];
}
} else if (this.isDeclarationOrJSX(path.parent) && path.isStringLiteral()) {
} else if (this.isValidParent(path.parent) && path.isStringLiteral()) {
return [
{
original: path.node,
Expand All @@ -36,7 +36,15 @@ export class StringLiteralMutator implements NodeMutator {
}
}

private isDeclarationOrJSX(parent?: types.Node): boolean {
return !types.isImportDeclaration(parent) && !types.isExportDeclaration(parent) && !types.isJSXAttribute(parent);
private isValidParent(parent?: types.Node): boolean {
return !(
types.isImportDeclaration(parent) ||
types.isExportDeclaration(parent) ||
types.isModuleDeclaration(parent) ||
types.isTSExternalModuleReference(parent) ||
types.isJSXAttribute(parent) ||
types.isExpressionStatement(parent) ||
types.isTSLiteralType(parent)
);
}
}
36 changes: 36 additions & 0 deletions packages/instrumenter/src/mutators/unary-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class UnaryOperatorMutator implements NodeMutator {
public name = 'UnaryOperator';

private readonly operators: { [targetedOperator: string]: string } = {
'+': '-',
'-': '+',
'~': '',
};

public mutate(path: NodePath): NodeMutation[] {
if (path.isUnaryExpression() && this.operators[path.node.operator] !== undefined && path.node.prefix) {
return this.operators[path.node.operator].length > 0
? [
{
original: path.node,
replacement: types.unaryExpression(this.operators[path.node.operator] as any, path.node.argument),
},
]
: [
{
original: path.node,
replacement: types.cloneNode(path.node.argument, false),
},
];
}

return [];
}
}
26 changes: 26 additions & 0 deletions packages/instrumenter/src/mutators/update-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';

import { NodeMutation } from '../mutant';

import { NodeMutator } from '.';

export class UpdateOperatorMutator implements NodeMutator {
public name = 'UpdateOperator';

private readonly operators: { [targetedOperator: string]: '--' | '++' } = {
'++': '--',
'--': '++',
};

public mutate(path: NodePath): NodeMutation[] {
return path.isUpdateExpression() && this.operators[path.node.operator] !== undefined
? [
{
original: path.node,
replacement: types.updateExpression(this.operators[path.node.operator], path.node.argument, path.node.prefix),
},
]
: [];
}
}
23 changes: 23 additions & 0 deletions packages/instrumenter/src/util/binary-operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type BinaryOperator =
| '+'
| '-'
| '/'
| '%'
| '*'
| '**'
| '&'
| '|'
| '>>'
| '>>>'
| '<<'
| '^'
| '=='
| '==='
| '!='
| '!=='
| 'in'
| 'instanceof'
| '>'
| '<'
| '>='
| '<=';
12 changes: 9 additions & 3 deletions packages/instrumenter/test/helpers/expect-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const plugins = [
'v8intrinsic',
'partialApplication',
['decorators', { decoratorsBeforeExport: false }],
'jsx',
'typescript',
] as ParserPlugin[];

export function expectJSMutation(sut: NodeMutator, originalCode: string, ...expectedReplacements: string[]) {
Expand All @@ -46,10 +48,14 @@ export function expectJSMutation(sut: NodeMutator, originalCode: string, ...expe
},
});
expect(mutants).lengthOf(expectedReplacements.length);
const actualReplacements = mutants.map(jsMutantToString);
const actualReplacements = mutants.map((mutant) => jsMutantToString(mutant, originalCode));
expectedReplacements.forEach((expected) => expect(actualReplacements, `was: ${actualReplacements.join(',')}`).to.include(expected));
}

function jsMutantToString(mutant: NodeMutation): string {
return generate(mutant.replacement).code;
function jsMutantToString(mutant: NodeMutation, originalCode: string): string {
const mutatedCode = generate(mutant.replacement).code;
const beforeMutatedCode = originalCode.substring(0, mutant.original.start || 0);
const afterMutatedCode = originalCode.substring(mutant.original.end || 0);

return `${beforeMutatedCode}${mutatedCode}${afterMutatedCode}`;
}
4 changes: 3 additions & 1 deletion packages/instrumenter/test/unit/mutant-placers/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ describe(placeMutant.name, () => {
path.node.loc = { start: { column: 3, line: 2 }, end: { column: 5, line: 4 } };
mutantPlacers[0].throws(expectedError);
const mutants = [createMutant()];
expect(() => placeMutant(path, mutants, [fooPlacer])).throws('Error while placing mutants on 2:3 with fooPlacer. Error: expectedError');
expect(() => placeMutant(path, mutants, [fooPlacer])).throws(
'Error while placing mutants of type(s) "fooMutator" on 2:3 with fooPlacer. Error: expectedError'
);
});
});
Loading

0 comments on commit 3b87743

Please sign in to comment.