From c7d150ab1af4341bb59381ef55fa54eff0113a11 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 17 Aug 2020 19:29:28 +0200 Subject: [PATCH] fix(instrumenter): support anonymous function names (#2388) 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 --- .../conditional-expression-mutant-placer.ts | 49 +++++++++- ...nditional-expression-mutant-placer.spec.ts | 94 +++++++++++++++++-- .../instrumenter/vue-sample.vue.out.snap | 32 +------ 3 files changed, 138 insertions(+), 37 deletions(-) diff --git a/packages/instrumenter/src/mutant-placers/conditional-expression-mutant-placer.ts b/packages/instrumenter/src/mutant-placers/conditional-expression-mutant-placer.ts index 554ec15379..d9109b464e 100644 --- a/packages/instrumenter/src/mutant-placers/conditional-expression-mutant-placer.ts +++ b/packages/instrumenter/src/mutant-placers/conditional-expression-mutant-placer.ts @@ -5,18 +5,63 @@ import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpress import { MutantPlacer } from './mutant-placer'; +function nameAnonymousClassOrFunctionExpression(path: NodePath) { + 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): NodePath { + 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) { + 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(path as NodePath, 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 diff --git a/packages/instrumenter/test/unit/mutant-placers/conditional-expression-mutant-placer.spec.ts b/packages/instrumenter/test/unit/mutant-placers/conditional-expression-mutant-placer.spec.ts index 42e4147f7d..92a8a9f635 100644 --- a/packages/instrumenter/test/unit/mutant-placers/conditional-expression-mutant-placer.spec.ts +++ b/packages/instrumenter/test/unit/mutant-placers/conditional-expression-mutant-placer.spec.ts @@ -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'; @@ -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.isTemplateLiteral()); @@ -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; }\)\(\)/); + }); + }); }); diff --git a/packages/instrumenter/testResources/instrumenter/vue-sample.vue.out.snap b/packages/instrumenter/testResources/instrumenter/vue-sample.vue.out.snap index d33c0e8c4b..147c123dec 100644 --- a/packages/instrumenter/testResources/instrumenter/vue-sample.vue.out.snap +++ b/packages/instrumenter/testResources/instrumenter/vue-sample.vue.out.snap @@ -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__) { @@ -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;