Skip to content

Commit

Permalink
feat(arrow mutations): add arrow mutations and refactor JavaScript mu…
Browse files Browse the repository at this point in the history
…tators (#1898)

* JavaScript mutator code improvements
  - use raw string mutations *internally* whenever possible
  - generate some of the mutations as AST nodes from the Babel types API
  - use some ternaries
  - remove some intermediate const declarations no longer needed
  - remove a couple of internal utility functions no longer needed
* add Arrow Function Mutator for JavaScript (already supported by the existing TypeScript mutator)
* remove `lodash.clonedeep` from dependencies in packages/javascript-mutator/package.json
  • Loading branch information
Chris Brody authored and simondel committed Dec 11, 2019
1 parent b619b97 commit 898d38b
Show file tree
Hide file tree
Showing 20 changed files with 146 additions and 156 deletions.
1 change: 0 additions & 1 deletion packages/javascript-mutator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@babel/parser": "~7.7.0",
"@babel/traverse": "~7.7.0",
"@stryker-mutator/api": "^2.4.0",
"lodash.clonedeep": "^4.5.0",
"tslib": "~1.10.0"
},
"peerDependencies": {
Expand Down
43 changes: 17 additions & 26 deletions packages/javascript-mutator/src/JavaScriptMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Mutant, Mutator } from '@stryker-mutator/api/mutant';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';

import BabelParser from './helpers/BabelParser';
import copy from './helpers/copy';
import { NodeMutator } from './mutators/NodeMutator';
import { NODE_MUTATORS_TOKEN, PARSER_TOKEN } from './helpers/tokens';

Expand All @@ -21,35 +20,27 @@ export class JavaScriptMutator implements Mutator {

this.parser.getNodes(ast).forEach(node => {
this.mutators.forEach(mutator => {
const mutatedNodes = mutator.mutate(node, copy);

if (mutatedNodes.length) {
const newMutants = this.generateMutants(mutatedNodes, mutator.name, file.name);
mutants.push(...newMutants);
}
const fileName = file.name;
const mutatorName = mutator.name;

mutator.mutate(node).forEach(([original, mutation]) => {
if (original.start !== null && original.end !== null) {
const replacement = types.isNode(mutation) ? this.parser.generateCode(mutation) : mutation.raw;

mutants.push({
fileName: fileName,
mutatorName: mutatorName,
range: [original.start, original.end],
replacement
});

this.log.trace(`Generated mutant for mutator ${mutatorName} in file ${fileName} with replacement code "${replacement}"`);
}
});
});
});
});

return mutants;
}

private generateMutants(mutatedNodes: types.Node[], mutatorName: string, fileName: string): Mutant[] {
const mutants: Mutant[] = [];
mutatedNodes.forEach(node => {
const replacement = this.parser.generateCode(node);
if (node.start !== null && node.end !== null) {
const range: [number, number] = [node.start, node.end];
const mutant: Mutant = {
fileName,
mutatorName,
range,
replacement
};
this.log.trace(`Generated mutant for mutator ${mutatorName} in file ${fileName} with replacement code "${replacement}"`);
mutants.push(mutant);
}
});
return mutants;
}
}
12 changes: 3 additions & 9 deletions packages/javascript-mutator/src/helpers/NodeGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import * as types from '@babel/types';

export class NodeGenerator {
public static createBooleanLiteralNode(originalNode: types.Node, value: boolean): types.BooleanLiteral {
public static createMutatedCloneWithProperties(originalNode: types.Node, props: { [key: string]: any }): types.Node {
return {
end: originalNode.end,
innerComments: originalNode.innerComments,
leadingComments: originalNode.leadingComments,
loc: originalNode.loc,
start: originalNode.start,
trailingComments: originalNode.trailingComments,
type: 'BooleanLiteral',
value
...originalNode,
...props
};
}
}
3 changes: 0 additions & 3 deletions packages/javascript-mutator/src/helpers/copy.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as types from '@babel/types';

import { NodeGenerator } from '../helpers/NodeGenerator';

import { NodeMutator } from './NodeMutator';

export default class ArithmeticOperatorMutator implements NodeMutator {
Expand All @@ -13,19 +15,18 @@ export default class ArithmeticOperatorMutator implements NodeMutator {

public name = 'ArithmeticOperator';

public mutate(node: types.Node, clone: <T extends types.Node>(node: T, deep?: boolean) => T): types.Node[] {
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
if (types.isBinaryExpression(node)) {
let mutatedOperators = this.operators[node.operator];
if (mutatedOperators) {
if (typeof mutatedOperators === 'string') {
mutatedOperators = [mutatedOperators];
}

return mutatedOperators.map<types.Node>(mutatedOperator => {
const mutatedNode = clone(node);
mutatedNode.operator = mutatedOperator as any;
return mutatedNode;
});
return mutatedOperators.map(mutatedOperator => [
node,
NodeGenerator.createMutatedCloneWithProperties(node, { operator: mutatedOperator as any })
]);
}
}

Expand Down
23 changes: 12 additions & 11 deletions packages/javascript-mutator/src/mutators/ArrayDeclarationMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import { NodeMutator } from './NodeMutator';
export default class ArrayDeclarationMutator implements NodeMutator {
public name = 'ArrayDeclaration';

public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
const nodes: types.Node[] = [];

public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
if (types.isArrayExpression(node)) {
const mutatedNode = copy(node);
mutatedNode.elements = node.elements.length ? [] : [types.stringLiteral('Stryker was here')];
nodes.push(mutatedNode);
return [
// replace [...]
node.elements.length
? [node, { raw: '[]' }] // raw string here
: [node, { raw: '["Stryker was here"]' }]
];
} else if ((types.isCallExpression(node) || types.isNewExpression(node)) && types.isIdentifier(node.callee) && node.callee.name === 'Array') {
const mutatedNode = copy(node);
mutatedNode.arguments = node.arguments.length ? [] : [types.arrayExpression()];
nodes.push(mutatedNode);
const newPrefix = types.isNewExpression(node) ? 'new ' : '';
const mutatedCallArgs = node.arguments && node.arguments.length ? '' : '[]';
return [[node, { raw: `${newPrefix}Array(${mutatedCallArgs})` }]];
} else {
return [];
}

return nodes;
}
}
16 changes: 16 additions & 0 deletions packages/javascript-mutator/src/mutators/ArrowFunctionMutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as types from '@babel/types';

import { NodeMutator } from './NodeMutator';

/**
* Represents a mutator which can remove the content of a Object.
*/
export default class ArrowFunctionMutator implements NodeMutator {
public name = 'ArrowFunction';

public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
return types.isArrowFunctionExpression(node) && !types.isBlockStatement(node.body)
? [[node, { raw: '() => undefined' }]] // raw string replacement
: [];
}
}
18 changes: 8 additions & 10 deletions packages/javascript-mutator/src/mutators/BlockStatementMutator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as types from '@babel/types';

import { NodeGenerator } from '../helpers/NodeGenerator';

import { NodeMutator } from './NodeMutator';

/**
Expand All @@ -8,15 +10,11 @@ import { NodeMutator } from './NodeMutator';
export default class BlockStatementMutator implements NodeMutator {
public name = 'BlockStatement';

public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
const nodes: types.Node[] = [];

if (types.isBlockStatement(node) && node.body.length > 0) {
const mutatedNode = copy(node);
mutatedNode.body = [];
nodes.push(mutatedNode);
}

return nodes;
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
return types.isBlockStatement(node) && node.body.length > 0
? [
[node, NodeGenerator.createMutatedCloneWithProperties(node, { body: [] })] // `{}`
]
: [];
}
}
16 changes: 6 additions & 10 deletions packages/javascript-mutator/src/mutators/BooleanLiteralMutator.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import * as types from '@babel/types';

import { NodeGenerator } from '../helpers/NodeGenerator';

import { NodeMutator } from './NodeMutator';

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

private readonly unaryBooleanPrefix = '!';

public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
const nodes: types.Node[] = [];

public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
// true -> false or false -> true
if (types.isBooleanLiteral(node)) {
const mutatedNode = copy(node);
mutatedNode.value = !mutatedNode.value;
nodes.push(mutatedNode);
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { value: !node.value })]];
} else if (types.isUnaryExpression(node) && node.operator === this.unaryBooleanPrefix && node.prefix) {
const mutatedNode = copy(node.argument);
mutatedNode.start = node.start;
nodes.push(mutatedNode);
return [[node, node.argument]];
}

return nodes;
return [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,33 @@ export default class ConditionalExpressionMutator implements NodeMutator {
return this.validOperators.includes(operator);
}

public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
if ((types.isBinaryExpression(node) || types.isLogicalExpression(node)) && this.hasValidParent(node) && this.isValidOperator(node.operator)) {
return [NodeGenerator.createBooleanLiteralNode(node, false), NodeGenerator.createBooleanLiteralNode(node, true)];
return [
// raw string mutations
[node, { raw: 'true' }],
[node, { raw: 'false' }]
];
} else if (types.isDoWhileStatement(node) || types.isWhileStatement(node)) {
return [NodeGenerator.createBooleanLiteralNode(node.test, false)];
return [[node.test, { raw: 'false' }]];
} else if (types.isForStatement(node)) {
if (!node.test) {
const mutatedNode = copy(node);
mutatedNode.test = NodeGenerator.createBooleanLiteralNode(node, false);
return [mutatedNode];
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { test: types.booleanLiteral(false) })]];
} else {
return [NodeGenerator.createBooleanLiteralNode(node.test, false)];
return [[node.test, { raw: 'false' }]];
}
} else if (types.isIfStatement(node)) {
return [NodeGenerator.createBooleanLiteralNode(node.test, false), NodeGenerator.createBooleanLiteralNode(node.test, true)];
return [
// raw string mutations in the `if` condition
[node.test, { raw: 'true' }],
[node.test, { raw: 'false' }]
];
} else if (
types.isSwitchCase(node) &&
// if not a fallthrough case
node.consequent.length > 0
) {
const mutatedNode = copy(node);
mutatedNode.consequent = [];
return [mutatedNode];
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { consequent: [] })]];
}

return [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as types from '@babel/types';

import { NodeGenerator } from '../helpers/NodeGenerator';

import { NodeMutator } from './NodeMutator';

export default class EqualityOperatorMutator implements NodeMutator {
Expand All @@ -16,19 +18,18 @@ export default class EqualityOperatorMutator implements NodeMutator {

public name = 'EqualityOperator';

public mutate(node: types.Node, clone: <T extends types.Node>(node: T, deep?: boolean) => T): types.Node[] {
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
if (types.isBinaryExpression(node)) {
let mutatedOperators = this.operators[node.operator];
if (mutatedOperators) {
if (typeof mutatedOperators === 'string') {
mutatedOperators = [mutatedOperators];
}

return mutatedOperators.map<types.Node>(mutatedOperator => {
const mutatedNode = clone(node);
mutatedNode.operator = mutatedOperator as any;
return mutatedNode;
});
return mutatedOperators.map(mutatedOperator => [
node,
NodeGenerator.createMutatedCloneWithProperties(node, { operator: mutatedOperator as any })
]);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as types from '@babel/types';

import { NodeGenerator } from '../helpers/NodeGenerator';

import { NodeMutator } from './NodeMutator';

export default class LogicalOperatorMutator implements NodeMutator {
Expand All @@ -10,19 +12,18 @@ export default class LogicalOperatorMutator implements NodeMutator {

public name = 'LogicalOperator';

public mutate(node: types.Node, clone: <T extends types.Node>(node: T, deep?: boolean) => T): types.Node[] {
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
if (types.isLogicalExpression(node)) {
let mutatedOperators = this.operators[node.operator];
if (mutatedOperators) {
if (typeof mutatedOperators === 'string') {
mutatedOperators = [mutatedOperators];
}

return mutatedOperators.map<types.Node>(mutatedOperator => {
const mutatedNode = clone(node);
mutatedNode.operator = mutatedOperator as any;
return mutatedNode;
});
return mutatedOperators.map(mutatedOperator => [
node,
NodeGenerator.createMutatedCloneWithProperties(node, { operator: mutatedOperator as any })
]);
}
}

Expand Down
15 changes: 10 additions & 5 deletions packages/javascript-mutator/src/mutators/NodeMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ export interface NodeMutator {
* Applies the Mutator to a Node. This can result in one or more mutated Nodes, or null if no mutation was applied.
* This method will be called on every node of the abstract syntax tree,
* implementing mutators should decide themselves if they want to mutate this specific node.
* If the mutator wants to mutate the node, it should return a clone of the node with mutations,
* otherwise null.
*
* If the mutator wants to mutate the node, it should return a array of mutated nodes,
* as an array of tuples that specify which node is replaced and
* what it should be replaced with (may be a raw string);
* otherwise an empty array.
*
* A mutated node may be based on a clone of the original node or just a brand new node.
*
* @param node A FROZEN Node which could be cloned and mutated.
* @param copy A function to create a copy of an object.
* @returns An array of mutated Nodes.
* @returns An array of mutations, as tuples.
*/
mutate(node: NodeWithParent, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[];
mutate(node: NodeWithParent): Array<[types.Node, types.Node | { raw: string }]>;
}
14 changes: 4 additions & 10 deletions packages/javascript-mutator/src/mutators/ObjectLiteralMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@ import { NodeMutator } from './NodeMutator';
export default class ObjectLiteralMutator implements NodeMutator {
public name = 'ObjectLiteral';

public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
const nodes: types.Node[] = [];

if (types.isObjectExpression(node) && node.properties.length > 0) {
const mutatedNode = copy(node);
mutatedNode.properties = [];
nodes.push(mutatedNode);
}

return nodes;
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
return types.isObjectExpression(node) && node.properties.length > 0
? [[node, { raw: '{}' }]] // raw string replacement
: [];
}
}
Loading

0 comments on commit 898d38b

Please sign in to comment.