-
Notifications
You must be signed in to change notification settings - Fork 251
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(instrumenter): Implement new mutant placing algorithm (#2964)
Ensure all mutants are only placed in the code once.
- Loading branch information
Showing
54 changed files
with
1,252 additions
and
1,378 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
43
packages/instrumenter/src/mutant-placers/statement-mutant-placer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
packages/instrumenter/src/mutant-placers/throw-placement-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.