Skip to content

Commit 898d38b

Browse files
Chris Brodysimondel
authored andcommitted
feat(arrow mutations): add arrow mutations and refactor JavaScript mutators (#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
1 parent b619b97 commit 898d38b

20 files changed

+146
-156
lines changed

packages/javascript-mutator/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
"@babel/parser": "~7.7.0",
4141
"@babel/traverse": "~7.7.0",
4242
"@stryker-mutator/api": "^2.4.0",
43-
"lodash.clonedeep": "^4.5.0",
4443
"tslib": "~1.10.0"
4544
},
4645
"peerDependencies": {

packages/javascript-mutator/src/JavaScriptMutator.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Mutant, Mutator } from '@stryker-mutator/api/mutant';
55
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
66

77
import BabelParser from './helpers/BabelParser';
8-
import copy from './helpers/copy';
98
import { NodeMutator } from './mutators/NodeMutator';
109
import { NODE_MUTATORS_TOKEN, PARSER_TOKEN } from './helpers/tokens';
1110

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

2221
this.parser.getNodes(ast).forEach(node => {
2322
this.mutators.forEach(mutator => {
24-
const mutatedNodes = mutator.mutate(node, copy);
25-
26-
if (mutatedNodes.length) {
27-
const newMutants = this.generateMutants(mutatedNodes, mutator.name, file.name);
28-
mutants.push(...newMutants);
29-
}
23+
const fileName = file.name;
24+
const mutatorName = mutator.name;
25+
26+
mutator.mutate(node).forEach(([original, mutation]) => {
27+
if (original.start !== null && original.end !== null) {
28+
const replacement = types.isNode(mutation) ? this.parser.generateCode(mutation) : mutation.raw;
29+
30+
mutants.push({
31+
fileName: fileName,
32+
mutatorName: mutatorName,
33+
range: [original.start, original.end],
34+
replacement
35+
});
36+
37+
this.log.trace(`Generated mutant for mutator ${mutatorName} in file ${fileName} with replacement code "${replacement}"`);
38+
}
39+
});
3040
});
3141
});
3242
});
3343

3444
return mutants;
3545
}
36-
37-
private generateMutants(mutatedNodes: types.Node[], mutatorName: string, fileName: string): Mutant[] {
38-
const mutants: Mutant[] = [];
39-
mutatedNodes.forEach(node => {
40-
const replacement = this.parser.generateCode(node);
41-
if (node.start !== null && node.end !== null) {
42-
const range: [number, number] = [node.start, node.end];
43-
const mutant: Mutant = {
44-
fileName,
45-
mutatorName,
46-
range,
47-
replacement
48-
};
49-
this.log.trace(`Generated mutant for mutator ${mutatorName} in file ${fileName} with replacement code "${replacement}"`);
50-
mutants.push(mutant);
51-
}
52-
});
53-
return mutants;
54-
}
5546
}
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import * as types from '@babel/types';
22

33
export class NodeGenerator {
4-
public static createBooleanLiteralNode(originalNode: types.Node, value: boolean): types.BooleanLiteral {
4+
public static createMutatedCloneWithProperties(originalNode: types.Node, props: { [key: string]: any }): types.Node {
55
return {
6-
end: originalNode.end,
7-
innerComments: originalNode.innerComments,
8-
leadingComments: originalNode.leadingComments,
9-
loc: originalNode.loc,
10-
start: originalNode.start,
11-
trailingComments: originalNode.trailingComments,
12-
type: 'BooleanLiteral',
13-
value
6+
...originalNode,
7+
...props
148
};
159
}
1610
}

packages/javascript-mutator/src/helpers/copy.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/javascript-mutator/src/mutators/ArithmeticOperatorMutator.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as types from '@babel/types';
22

3+
import { NodeGenerator } from '../helpers/NodeGenerator';
4+
35
import { NodeMutator } from './NodeMutator';
46

57
export default class ArithmeticOperatorMutator implements NodeMutator {
@@ -13,19 +15,18 @@ export default class ArithmeticOperatorMutator implements NodeMutator {
1315

1416
public name = 'ArithmeticOperator';
1517

16-
public mutate(node: types.Node, clone: <T extends types.Node>(node: T, deep?: boolean) => T): types.Node[] {
18+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
1719
if (types.isBinaryExpression(node)) {
1820
let mutatedOperators = this.operators[node.operator];
1921
if (mutatedOperators) {
2022
if (typeof mutatedOperators === 'string') {
2123
mutatedOperators = [mutatedOperators];
2224
}
2325

24-
return mutatedOperators.map<types.Node>(mutatedOperator => {
25-
const mutatedNode = clone(node);
26-
mutatedNode.operator = mutatedOperator as any;
27-
return mutatedNode;
28-
});
26+
return mutatedOperators.map(mutatedOperator => [
27+
node,
28+
NodeGenerator.createMutatedCloneWithProperties(node, { operator: mutatedOperator as any })
29+
]);
2930
}
3031
}
3132

packages/javascript-mutator/src/mutators/ArrayDeclarationMutator.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@ import { NodeMutator } from './NodeMutator';
88
export default class ArrayDeclarationMutator implements NodeMutator {
99
public name = 'ArrayDeclaration';
1010

11-
public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
12-
const nodes: types.Node[] = [];
13-
11+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
1412
if (types.isArrayExpression(node)) {
15-
const mutatedNode = copy(node);
16-
mutatedNode.elements = node.elements.length ? [] : [types.stringLiteral('Stryker was here')];
17-
nodes.push(mutatedNode);
13+
return [
14+
// replace [...]
15+
node.elements.length
16+
? [node, { raw: '[]' }] // raw string here
17+
: [node, { raw: '["Stryker was here"]' }]
18+
];
1819
} else if ((types.isCallExpression(node) || types.isNewExpression(node)) && types.isIdentifier(node.callee) && node.callee.name === 'Array') {
19-
const mutatedNode = copy(node);
20-
mutatedNode.arguments = node.arguments.length ? [] : [types.arrayExpression()];
21-
nodes.push(mutatedNode);
20+
const newPrefix = types.isNewExpression(node) ? 'new ' : '';
21+
const mutatedCallArgs = node.arguments && node.arguments.length ? '' : '[]';
22+
return [[node, { raw: `${newPrefix}Array(${mutatedCallArgs})` }]];
23+
} else {
24+
return [];
2225
}
23-
24-
return nodes;
2526
}
2627
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as types from '@babel/types';
2+
3+
import { NodeMutator } from './NodeMutator';
4+
5+
/**
6+
* Represents a mutator which can remove the content of a Object.
7+
*/
8+
export default class ArrowFunctionMutator implements NodeMutator {
9+
public name = 'ArrowFunction';
10+
11+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
12+
return types.isArrowFunctionExpression(node) && !types.isBlockStatement(node.body)
13+
? [[node, { raw: '() => undefined' }]] // raw string replacement
14+
: [];
15+
}
16+
}
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as types from '@babel/types';
22

3+
import { NodeGenerator } from '../helpers/NodeGenerator';
4+
35
import { NodeMutator } from './NodeMutator';
46

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

11-
public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
12-
const nodes: types.Node[] = [];
13-
14-
if (types.isBlockStatement(node) && node.body.length > 0) {
15-
const mutatedNode = copy(node);
16-
mutatedNode.body = [];
17-
nodes.push(mutatedNode);
18-
}
19-
20-
return nodes;
13+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
14+
return types.isBlockStatement(node) && node.body.length > 0
15+
? [
16+
[node, NodeGenerator.createMutatedCloneWithProperties(node, { body: [] })] // `{}`
17+
]
18+
: [];
2119
}
2220
}
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
11
import * as types from '@babel/types';
22

3+
import { NodeGenerator } from '../helpers/NodeGenerator';
4+
35
import { NodeMutator } from './NodeMutator';
46

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

810
private readonly unaryBooleanPrefix = '!';
911

10-
public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
11-
const nodes: types.Node[] = [];
12-
12+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
1313
// true -> false or false -> true
1414
if (types.isBooleanLiteral(node)) {
15-
const mutatedNode = copy(node);
16-
mutatedNode.value = !mutatedNode.value;
17-
nodes.push(mutatedNode);
15+
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { value: !node.value })]];
1816
} else if (types.isUnaryExpression(node) && node.operator === this.unaryBooleanPrefix && node.prefix) {
19-
const mutatedNode = copy(node.argument);
20-
mutatedNode.start = node.start;
21-
nodes.push(mutatedNode);
17+
return [[node, node.argument]];
2218
}
2319

24-
return nodes;
20+
return [];
2521
}
2622
}

packages/javascript-mutator/src/mutators/ConditionalExpressionMutator.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,33 @@ export default class ConditionalExpressionMutator implements NodeMutator {
2929
return this.validOperators.includes(operator);
3030
}
3131

32-
public mutate(node: types.Node, copy: <T extends types.Node>(obj: T, deep?: boolean) => T): types.Node[] {
32+
public mutate(node: types.Node): Array<[types.Node, types.Node | { raw: string }]> {
3333
if ((types.isBinaryExpression(node) || types.isLogicalExpression(node)) && this.hasValidParent(node) && this.isValidOperator(node.operator)) {
34-
return [NodeGenerator.createBooleanLiteralNode(node, false), NodeGenerator.createBooleanLiteralNode(node, true)];
34+
return [
35+
// raw string mutations
36+
[node, { raw: 'true' }],
37+
[node, { raw: 'false' }]
38+
];
3539
} else if (types.isDoWhileStatement(node) || types.isWhileStatement(node)) {
36-
return [NodeGenerator.createBooleanLiteralNode(node.test, false)];
40+
return [[node.test, { raw: 'false' }]];
3741
} else if (types.isForStatement(node)) {
3842
if (!node.test) {
39-
const mutatedNode = copy(node);
40-
mutatedNode.test = NodeGenerator.createBooleanLiteralNode(node, false);
41-
return [mutatedNode];
43+
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { test: types.booleanLiteral(false) })]];
4244
} else {
43-
return [NodeGenerator.createBooleanLiteralNode(node.test, false)];
45+
return [[node.test, { raw: 'false' }]];
4446
}
4547
} else if (types.isIfStatement(node)) {
46-
return [NodeGenerator.createBooleanLiteralNode(node.test, false), NodeGenerator.createBooleanLiteralNode(node.test, true)];
48+
return [
49+
// raw string mutations in the `if` condition
50+
[node.test, { raw: 'true' }],
51+
[node.test, { raw: 'false' }]
52+
];
4753
} else if (
4854
types.isSwitchCase(node) &&
4955
// if not a fallthrough case
5056
node.consequent.length > 0
5157
) {
52-
const mutatedNode = copy(node);
53-
mutatedNode.consequent = [];
54-
return [mutatedNode];
58+
return [[node, NodeGenerator.createMutatedCloneWithProperties(node, { consequent: [] })]];
5559
}
5660

5761
return [];

0 commit comments

Comments
 (0)