Skip to content

Commit

Permalink
fix(instrumenter): support anonymous function names (#2388)
Browse files Browse the repository at this point in the history
Do a best-effort attempt to minimize side effects from instrumentation by maintaining the `fn.name` property when placing mutants in code.

Instrument these:

```ts
const foo = function () { }
const bar = () => {}
const Baz = class { }
const foo = { bar: function () { } }
```

Like this:

```ts
const foo = true? function foo () { }: ...;
const bar = true? (() => { const bar = () =>{}; return bar } {})() : ...;
const Baz = true? class Baz { } : ...;
const foo = true? function bar() {}
```

Fixes #2362
  • Loading branch information
nicojs authored Aug 17, 2020
1 parent 9f818f0 commit c7d150a
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,63 @@ import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpress

import { MutantPlacer } from './mutant-placer';

function nameAnonymousClassOrFunctionExpression(path: NodePath<types.Expression>) {
if ((path.isFunctionExpression() || path.isClassExpression()) && !path.node.id) {
if (path.parentPath.isVariableDeclarator() && types.isIdentifier(path.parentPath.node.id)) {
path.node.id = path.parentPath.node.id;
} else if (
path.parentPath.isObjectProperty() &&
types.isIdentifier(path.parentPath.node.key) &&
path.getStatementParent().isVariableDeclaration()
) {
path.node.id = path.parentPath.node.key;
}
}
}

function nameIfAnonymous(path: NodePath<types.Expression>): NodePath<types.Expression> {
nameAnonymousClassOrFunctionExpression(path);
if (path.isArrowFunctionExpression() && path.parentPath.isVariableDeclarator() && types.isIdentifier(path.parentPath.node.id)) {
path.replaceWith(
types.callExpression(
types.arrowFunctionExpression(
[],
types.blockStatement([
types.variableDeclaration('const', [types.variableDeclarator(path.parentPath.node.id, path.node)]),
types.returnStatement(path.parentPath.node.id),
])
),
[]
)
);
}

return path;
}

function isValidParent(child: NodePath<types.Expression>) {
const parent = child.parentPath;
return !isObjectPropertyKey() && !parent.isTaggedTemplateExpression();
function isObjectPropertyKey() {
return parent.isObjectProperty() && parent.node.key === child.node;
}
}

/**
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : regularCode`;
*/
const conditionalExpressionMutantPlacer: MutantPlacer = (path: NodePath, mutants: Mutant[]): boolean => {
if (path.isExpression() && !path.parentPath.isObjectProperty() && !path.parentPath.isTaggedTemplateExpression()) {
if (path.isExpression() && isValidParent(path)) {
// First calculated the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
mutant,
ast: createMutatedAst<types.BinaryExpression>(path as NodePath<types.BinaryExpression>, mutant),
}));

// Second add the mutation coverage expression
// Make sure anonymous functions and classes keep their 'name' property
path.replaceWith(nameIfAnonymous(path));

// Add the mutation coverage expression
path.replaceWith(mutationCoverageSequenceExpression(mutants, path.node));

// Now apply the mutants
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { types } from '@babel/core';
import { types, NodePath } from '@babel/core';
import { normalizeWhitespaces } from '@stryker-mutator/util';
import generate from '@babel/generator';

Expand All @@ -13,12 +13,6 @@ describe(conditionalExpressionMutantPlacer.name, () => {
expect(conditionalExpressionMutantPlacer.name).eq('conditionalExpressionMutantPlacer');
});

it('should not place when the parent is an object literal', () => {
// A stringLiteral is considered an expression, while it is not save to place a mutant there!
const stringLiteral = findNodePath(parseJS("const foo = { 'foo': bar }"), (p) => p.isStringLiteral());
expect(conditionalExpressionMutantPlacer(stringLiteral, [])).false;
});

it('should not place when the parent is tagged template expression', () => {
// A templateLiteral is considered an expression, while it is not save to place a mutant there!
const templateLiteral = findNodePath(parseJS('html`<p></p>`'), (p) => p.isTemplateLiteral());
Expand Down Expand Up @@ -92,4 +86,90 @@ describe(conditionalExpressionMutantPlacer.name, () => {
// Assert
expect(actualCode).contains('const foo = __global_69fa48.__activeMutant__ === 659 ? bar : __global_69fa48.__activeMutant__ === 52 ? bar - baz');
});

describe('object literals', () => {
it('should not place when the expression is a key', () => {
// A stringLiteral is considered an expression, while it is not save to place a mutant there!
const stringLiteral = findNodePath(parseJS("const foo = { 'foo': bar }"), (p) => p.isStringLiteral());
expect(conditionalExpressionMutantPlacer(stringLiteral, [])).false;
});

it('should place when the expression is the value', () => {
// A stringLiteral is considered an expression, while it is not save to place a mutant there!
const stringLiteral = findNodePath(parseJS("const foo = { 'foo': bar }"), (p) => p.isIdentifier() && p.node.name === 'bar');
expect(conditionalExpressionMutantPlacer(stringLiteral, [])).true;
});
});

/**
* This describe has tests for anonymous classes and functions.
* @see https://github.com/stryker-mutator/stryker/issues/2362
*/
describe('anonymous expressions', () => {
function arrangeActAssert(ast: types.File, expression: NodePath, expectedMatch: RegExp) {
const mutants = [
createMutant({
id: 4,
original: expression.node,
replacement: types.identifier('bar'),
}),
];

// Act
conditionalExpressionMutantPlacer(expression, mutants);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actualCode).matches(expectedMatch);
}

it('should set the name of an anonymous function expression', () => {
// Arrange
const ast = parseJS('const foo = function () { }');
const functionExpression = findNodePath(ast, (p) => p.isFunctionExpression());
arrangeActAssert(ast, functionExpression, /const foo =.*function foo\(\) {}/);
});

it('should set the name of an anonymous method expression', () => {
// Arrange
const ast = parseJS('const foo = { bar: function () { } }');
const functionExpression = findNodePath(ast, (p) => p.isFunctionExpression());
arrangeActAssert(ast, functionExpression, /const foo =.*bar:.*function bar\(\) {}/);
});

it('should not set the name if the statement is not a variable declaration', () => {
// Arrange
const ast = parseJS('foo.bar = function () { }');
const functionExpression = findNodePath(ast, (p) => p.isFunctionExpression());
arrangeActAssert(ast, functionExpression, /foo\.bar =.*function \(\) {}/);
});

it('should not set the name of a named function expression', () => {
// Arrange
const ast = parseJS('const foo = function bar () { }');
const functionExpression = findNodePath(ast, (p) => p.isFunctionExpression());
arrangeActAssert(ast, functionExpression, /const foo =.*function bar\(\) {}/);
});

it('should set the name of an anonymous class expression', () => {
// Arrange
const ast = parseJS('const Foo = class { }');
const classExpression = findNodePath(ast, (p) => p.isClassExpression());
arrangeActAssert(ast, classExpression, /const Foo =.*class Foo {}/);
});

it('should not override the name of a named class expression', () => {
// Arrange
const ast = parseJS('const Foo = class Bar { }');
const classExpression = findNodePath(ast, (p) => p.isClassExpression());
arrangeActAssert(ast, classExpression, /const Foo =.*class Bar {}/);
});

it('should set the name of an anonymous arrow function', () => {
// Arrange
const ast = parseJS('const bar = () => {}');
const functionExpression = findNodePath(ast, (p) => p.isArrowFunctionExpression());
arrangeActAssert(ast, functionExpression, /const bar =.*\(\(\) => { const bar = \(\) => {}; return bar; }\)\(\)/);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ var __global_69fa48 = function (g) {
return g;
}(new Function(\\"return this\\")());
export default __global_69fa48.__activeMutant__ === 1 ? {
name: \\"\\",
export default __global_69fa48.__activeMutant__ === 0 ? {} : (__global_69fa48.__coverMutant__(0), {
name: __global_69fa48.__activeMutant__ === 1 ? \\"\\" : (__global_69fa48.__coverMutant__(1), 'HelloWorld'),
data() {
switch (__global_69fa48.__activeMutant__) {
Expand All @@ -127,32 +127,8 @@ export default __global_69fa48.__activeMutant__ === 1 ? {
default:
__global_69fa48.__coverMutant__(2);
{
return __global_69fa48.__activeMutant__ === 4 ? {
msg: \\"\\"
} : __global_69fa48.__activeMutant__ === 3 ? {} : (__global_69fa48.__coverMutant__(3, 4), {
msg: 'Welcome to Your Vue.js App'
});
}
break;
}
}
} : __global_69fa48.__activeMutant__ === 0 ? {} : (__global_69fa48.__coverMutant__(0, 1), {
name: 'HelloWorld',
data() {
switch (__global_69fa48.__activeMutant__) {
case 2:
{}
break;
default:
__global_69fa48.__coverMutant__(2);
{
return __global_69fa48.__activeMutant__ === 4 ? {
msg: \\"\\"
} : __global_69fa48.__activeMutant__ === 3 ? {} : (__global_69fa48.__coverMutant__(3, 4), {
msg: 'Welcome to Your Vue.js App'
return __global_69fa48.__activeMutant__ === 3 ? {} : (__global_69fa48.__coverMutant__(3), {
msg: __global_69fa48.__activeMutant__ === 4 ? \\"\\" : (__global_69fa48.__coverMutant__(4), 'Welcome to Your Vue.js App')
});
}
break;
Expand Down

0 comments on commit c7d150a

Please sign in to comment.