Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/new mutant placing #2964

Merged
merged 7 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { NodePath, types } from '@babel/core';

import { Mutant } from '../mutant';
import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpression } from '../util/syntax-helpers';
import { mutantTestExpression, mutationCoverageSequenceExpression } from '../util/syntax-helpers';

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

function nameAnonymousClassOrFunctionExpression(path: NodePath<types.Expression>) {
/**
* Will set the identifier of anonymous function expressions if is located in a variable declaration.
* Will treat input as readonly. Returns undefined if not needed.
* @example
* const a = function() { }
* becomes
* const a = function a() {}
*/
function classOrFunctionExpressionNamedIfNeeded(path: NodePath<types.Expression>): types.Expression | undefined {
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;
Expand All @@ -17,26 +24,35 @@ function nameAnonymousClassOrFunctionExpression(path: NodePath<types.Expression>
path.node.id = path.parentPath.node.key;
}
}
return;
}

function nameIfAnonymous(path: NodePath<types.Expression>): NodePath<types.Expression> {
nameAnonymousClassOrFunctionExpression(path);
/**
* Will set the identifier of anonymous arrow function expressions if is located in a variable declaration.
* Will treat input as readonly. Returns undefined if not needed.
* @example
* const a = () => { }
* becomes
* const a = (() => { const a = () => {}; return a; })()
*/
function arrowFunctionExpressionNamedIfNeeded(path: NodePath<types.Expression>): types.Expression | undefined {
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 types.callExpression(
types.arrowFunctionExpression(
[],
types.blockStatement([
types.variableDeclaration('const', [types.variableDeclarator(path.parentPath.node.id, path.node)]),
types.returnStatement(path.parentPath.node.id),
])
),
[]
);
}
return;
}

return path;
function nameIfAnonymous(path: NodePath<types.Expression>): types.Expression {
return classOrFunctionExpressionNamedIfNeeded(path) ?? arrowFunctionExpressionNamedIfNeeded(path) ?? path.node;
}

function isValidParent(child: NodePath<types.Expression>) {
Expand All @@ -48,31 +64,24 @@ function isValidParent(child: NodePath<types.Expression>) {
}

/**
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : regularCode`;
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : originalCode`;
*/
const expressionMutantPlacer: MutantPlacer = (path: NodePath, mutants: Mutant[]): boolean => {
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),
}));

export const expressionMutantPlacer: MutantPlacer<types.Expression> = {
name: 'expressionMutantPlacer',
canPlace(path) {
return path.isExpression() && isValidParent(path);
},
place(path, appliedMutants) {
// Make sure anonymous functions and classes keep their 'name' property
path.replaceWith(nameIfAnonymous(path));
let expression = nameIfAnonymous(path);

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

// Now apply the mutants
for (const appliedMutant of appliedMutants) {
path.replaceWith(types.conditionalExpression(mutantTestExpression(appliedMutant.mutant.id), appliedMutant.ast, path.node));
for (const [mutant, appliedMutant] of appliedMutants) {
expression = types.conditionalExpression(mutantTestExpression(mutant.id), appliedMutant, expression);
}
return true;
} else {
return false;
}
path.replaceWith(expression);
},
};

// Export it after initializing so `fn.name` is properly set
export { expressionMutantPlacer };
48 changes: 4 additions & 44 deletions packages/instrumenter/src/mutant-placers/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,8 @@
import path from 'path';

export * from './mutant-placer';
import { NodePath } from '@babel/core';

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

import { expressionMutantPlacer } from './expression-mutant-placer';
import { MutantPlacer } from './mutant-placer';
import { statementMutantPlacer } from './statement-mutant-placer';
import { expressionMutantPlacer } from './expression-mutant-placer';
import { switchCaseMutantPlacer } from './switch-case-mutant-placer';

export const MUTANT_PLACERS = Object.freeze([expressionMutantPlacer, statementMutantPlacer, switchCaseMutantPlacer]);

/**
* Represents a mutant placer, tries to place a mutant in the AST with corresponding mutation switch and mutant covering expression
* @see https://github.com/stryker-mutator/stryker-js/issues/1514
* @param node The ast node to try and replace with a mutated
* @param mutants The mutants to place in the AST node
* @param fileName The name of the file where the mutants are placed
* @param mutantPlacers The mutant placers to use (for unit testing purposes)
*/
export function placeMutants(node: NodePath, mutants: Mutant[], fileName: string, mutantPlacers: readonly MutantPlacer[] = MUTANT_PLACERS): boolean {
if (mutants.length) {
for (const placer of mutantPlacers) {
try {
if (placer(node, mutants)) {
return true;
}
} catch (error) {
const location = `${path.relative(process.cwd(), fileName)}:${node.node.loc?.start.line}:${node.node.loc?.start.column}`;
const message = `${placer.name} could not place mutants with type(s): "${mutants.map((mutant) => mutant.mutatorName).join(', ')}"`;
const errorMessage = `${location} ${message}. Either remove this file from the list of files to be mutated, or ignore the mutators. Please report this issue at https://github.com/stryker-mutator/stryker-js/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug_report.md&title=${encodeURIComponent(
message
)}.`;
let builtError = new Error(errorMessage);
try {
// `buildCodeFrameError` is kind of flaky, see https://github.com/stryker-mutator/stryker-js/issues/2695
builtError = node.buildCodeFrameError(errorMessage);
} catch {
// Idle, regular error will have to suffice
}
throw builtError;
}
}
}
return false;
}
export * from './mutant-placer';
export * from './throw-placement-error';
export const allMutantPlacers: readonly MutantPlacer[] = Object.freeze([expressionMutantPlacer, statementMutantPlacer, switchCaseMutantPlacer]);
7 changes: 6 additions & 1 deletion packages/instrumenter/src/mutant-placers/mutant-placer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { NodePath } from '@babel/core';
import * as types from '@babel/types';

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

export type MutantPlacer = (node: NodePath, mutants: Mutant[]) => boolean;
export interface MutantPlacer<TNode extends types.Node = types.Node> {
name: string;
canPlace(path: NodePath): boolean;
place(path: NodePath<TNode>, appliedMutants: Map<Mutant, TNode>): void;
}
43 changes: 15 additions & 28 deletions packages/instrumenter/src/mutant-placers/statement-mutant-placer.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,26 @@
import { types } from '@babel/core';

import { mutantTestExpression, createMutatedAst, mutationCoverageSequenceExpression } from '../util/syntax-helpers';
import { mutantTestExpression, mutationCoverageSequenceExpression } from '../util/syntax-helpers';

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

/**
* Mutant placer that places mutants in statements that allow it.
* It uses an `if` statement to do so
*/
const statementMutantPlacer: MutantPlacer = (path, mutants) => {
if (path.isStatement()) {
// First transform the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
mutant,
ast: createMutatedAst(path, mutant),
}));

const instrumentedAst = appliedMutants.reduce(
// Add if statements per mutant
(prev: types.Statement, { ast, mutant }) => types.ifStatement(mutantTestExpression(mutant.id), types.blockStatement([ast]), prev),
path.isBlockStatement()
? types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), ...path.node.body])
: types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), path.node])
);
if (path.isBlockStatement()) {
path.replaceWith(types.blockStatement([instrumentedAst]));
} else {
path.replaceWith(instrumentedAst);
export const statementMutantPlacer: MutantPlacer<types.Statement> = {
name: 'statementMutantPlacer',
canPlace(path) {
return path.isStatement();
},
place(path, appliedMutants) {
let statement: types.Statement = types.blockStatement([
types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())),
...(path.isBlockStatement() ? path.node.body : [path.node]),
]);
for (const [mutant, appliedMutant] of appliedMutants) {
statement = types.ifStatement(mutantTestExpression(mutant.id), types.blockStatement([appliedMutant]), statement);
}

return true;
} else {
return false;
}
path.replaceWith(path.isBlockStatement() ? types.blockStatement([statement]) : statement);
},
};

// Export it after initializing so `fn.name` is properly set
export { statementMutantPlacer };
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { types } from '@babel/core';

import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpression } from '../util';
import { mutantTestExpression, mutationCoverageSequenceExpression } from '../util';

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

Expand All @@ -14,31 +14,19 @@ import { MutantPlacer } from './mutant-placer';
* break;
* }
*/
const switchCaseMutantPlacer: MutantPlacer = (path, mutants): boolean => {
if (path.isSwitchCase()) {
// First transform the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => {
const ast = createMutatedAst(path, mutant);
if (!types.isSwitchCase(ast)) {
throw new Error(`${switchCaseMutantPlacer.name} can only place SwitchCase syntax`);
}
return {
ast,
mutant,
};
});

const instrumentedConsequent = appliedMutants.reduce(
// Add if statements per mutant
(prev: types.Statement, { ast, mutant }) => types.ifStatement(mutantTestExpression(mutant.id), types.blockStatement(ast.consequent), prev),
types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), ...path.node.consequent])
);
path.replaceWith(types.switchCase(path.node.test, [instrumentedConsequent]));
return true;
}

return false;
export const switchCaseMutantPlacer: MutantPlacer<types.SwitchCase> = {
name: 'switchCaseMutantPlacer',
canPlace(path) {
return path.isSwitchCase();
},
place(path, appliedMutants) {
let consequence: types.Statement = types.blockStatement([
types.expressionStatement(mutationCoverageSequenceExpression(appliedMutants.keys())),
...path.node.consequent,
]);
for (const [mutant, appliedMutant] of appliedMutants) {
consequence = types.ifStatement(mutantTestExpression(mutant.id), types.blockStatement(appliedMutant.consequent), consequence);
}
path.replaceWith(types.switchCase(path.node.test, [consequence]));
},
};

// Export it after initializing so `fn.name` is properly set
export { switchCaseMutantPlacer };
28 changes: 28 additions & 0 deletions packages/instrumenter/src/mutant-placers/throw-placement-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import path from 'path';

import { NodePath } from '@babel/core';
import { PropertyPathBuilder } from '@stryker-mutator/util';
import { StrykerOptions } from '@stryker-mutator/api/core';

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

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

export function throwPlacementError(error: Error, nodePath: NodePath, placer: MutantPlacer, mutants: Mutant[], fileName: string): never {
const location = `${path.relative(process.cwd(), fileName)}:${nodePath.node.loc?.start.line}:${nodePath.node.loc?.start.column}`;
const message = `${placer.name} could not place mutants with type(s): "${mutants.map((mutant) => mutant.mutatorName).join(', ')}"`;
const errorMessage = `${location} ${message}. Either remove this file from the list of files to be mutated, or exclude the mutator (using ${PropertyPathBuilder.create<StrykerOptions>()
.prop('mutator')
.prop('excludedMutations')
.build()}). Please report this issue at https://github.com/stryker-mutator/stryker-js/issues/new?assignees=&labels=%F0%9F%90%9B+Bug&template=bug_report.md&title=${encodeURIComponent(
message
)}. Original error: ${error.stack}`;
let builtError = new Error(errorMessage);
try {
// `buildCodeFrameError` is kind of flaky, see https://github.com/stryker-mutator/stryker-js/issues/2695
builtError = nodePath.buildCodeFrameError(errorMessage);
} catch {
// Idle, regular error will have to suffice
}
throw builtError;
}
Loading