Skip to content

Commit

Permalink
feat(instrumenter): add parsers (#2222)
Browse files Browse the repository at this point in the history
Add parsers to the instrumenter package. 

Add support for:

* Parsing `*.js`, `*.tsx`, `*.html` and `*.vue` files.
* Load custom babel config with `babel.config` format and `.babelrc` format.
  • Loading branch information
nicojs committed May 25, 2020
1 parent ba43da3 commit 3b57ef2
Show file tree
Hide file tree
Showing 44 changed files with 8,287 additions and 13 deletions.
4 changes: 2 additions & 2 deletions packages/instrumenter/.mocharc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dist/test/helpers/setupTests.js"
],
"spec": [
"dist/test/integration/**/*.js",
"dist/test/unit/**/*.js"
"dist/test/unit/**/*.js",
"dist/test/integration/**/*.js"
]
}
9 changes: 8 additions & 1 deletion packages/instrumenter/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@
],
"skipFiles": [
"<node_internals>/**"
]
],
"args": [
"--no-timeout"
],
"env": {
// Set CHAI_JEST_SNAPSHOT_UPDATE_ALL variable when you want to update the snapshot files
// "CHAI_JEST_SNAPSHOT_UPDATE_ALL": "true"
}
}
]
}
19 changes: 16 additions & 3 deletions packages/instrumenter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "dist/src/main.js",
"scripts": {
"test": "nyc --exclude-after-remap=false --check-coverage --reporter=html --report-dir=reports/coverage --lines 80 --functions 80 --branches 75 npm run mocha",
"mocha": "mocha"
"mocha": "mocha",
"stryker": "node ../core/bin/stryker run"
},
"repository": {
"type": "git",
Expand All @@ -29,10 +30,22 @@
},
"homepage": "https://stryker-mutator.io",
"dependencies": {
"@babel/core": "^7.9.6",
"@babel/generator": "^7.9.6",
"@babel/parser": "^7.9.6",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/preset-typescript": "^7.9.0",
"@stryker-mutator/api": "^3.2.2",
"@stryker-mutator/util": "^3.2.2"
"@stryker-mutator/util": "^3.2.2",
"angular-html-parser": "^1.7.0"
},
"devDependencies": {
"@stryker-mutator/test-helpers": "^3.2.2"
"@babel/preset-react": "^7.9.4",
"@stryker-mutator/test-helpers": "^3.2.2",
"@types/babel__core": "^7.1.7",
"@types/babel__generator": "^7.6.1",
"@types/babel__parser": "^7.1.1",
"@types/chai-jest-snapshot": "^1.3.5",
"chai-jest-snapshot": "^2.0.0"
}
}
41 changes: 41 additions & 0 deletions packages/instrumenter/src/mutant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { types } from '@babel/core';
import generate from '@babel/generator';

import { isLineBreak } from './util/char-helpers';

export interface NodeMutation {
replacement: types.Node;
original: types.Node;
}
export interface NamedNodeMutation extends NodeMutation {
mutatorName: string;
}

export class Mutant {
constructor(public id: number, public original: types.Node, public replacement: types.Node, public fileName: string, public mutatorName: string) {}

public originalLines(originalText: string): string {
const [startIndex, endIndex] = this.getMutationLineIndexes(originalText);
return originalText.substring(startIndex, endIndex);
}

private getMutationLineIndexes(originalText: string): [number, number] {
let startIndexLines = this.original.start!;
let endIndexLines = this.original.end!;
while (startIndexLines > 0 && !isLineBreak(originalText.charCodeAt(startIndexLines - 1))) {
startIndexLines--;
}
while (endIndexLines < originalText.length && !isLineBreak(originalText.charCodeAt(endIndexLines))) {
endIndexLines++;
}
return [startIndexLines, endIndexLines];
}

public mutatedLines(originalText: string): string {
const [startIndex, endIndex] = this.getMutationLineIndexes(originalText);
return `${originalText.substring(startIndex, this.original.start!)}${generate(this.replacement).code}${originalText.substring(
this.original.end!,
endIndex
)}`;
}
}
55 changes: 55 additions & 0 deletions packages/instrumenter/src/mutators/arithmetic-operator-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NodePath, types } from '@babel/core';

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

import { NodeMutator } from './node-mutator';

type Operator =
| '+'
| '-'
| '/'
| '%'
| '*'
| '**'
| '&'
| '|'
| '>>'
| '>>>'
| '<<'
| '^'
| '=='
| '==='
| '!='
| '!=='
| 'in'
| 'instanceof'
| '>'
| '<'
| '>='
| '<=';

export class ArithmeticOperatorMutator implements NodeMutator {
private readonly operators: {
[op: string]: Operator | undefined;
} = Object.freeze({
'+': '-',
'-': '+',
'*': '/',
'/': '*',
'%': '*',
} as const);

public name = 'ArithmeticOperator';

public mutate(path: NodePath): NodeMutation[] {
if (types.isBinaryExpression(path.node)) {
const mutatedOperator = this.operators[path.node.operator];
if (mutatedOperator) {
const replacement = types.cloneNode(path.node, false);
replacement.operator = mutatedOperator;
return [{ original: path.node, replacement }];
}
}
return [];
}
}
19 changes: 19 additions & 0 deletions packages/instrumenter/src/mutators/block-statement-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { types, NodePath } from '@babel/core';

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

import { NodeMutator } from './node-mutator';

export class BlockStatementMutator implements NodeMutator {
public name = 'BlockStatement';

public mutate(path: NodePath): NodeMutation[] {
if (path.isBlockStatement()) {
const replacement = types.cloneNode(path.node, false);
replacement.body = [];
return [{ original: path.node, replacement }];
} else {
return [];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NodePath, types } from '@babel/core';

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

import { NodeMutator } from './node-mutator';

export class ConditionalExpressionMutator implements NodeMutator {
private readonly validOperators: string[] = ['!=', '!==', '&&', '<', '<=', '==', '===', '>', '>=', '||'];

public name = 'ConditionalExpression';

private hasValidParent(node: NodePath): boolean {
return (
!node.parent ||
!(
types.isForStatement(node.parent) ||
types.isWhileStatement(node.parent) ||
types.isIfStatement(node.parent) ||
types.isDoWhileStatement(node.parent)
)
);
}

private isValidOperator(operator: string): boolean {
return this.validOperators.includes(operator);
}

public mutate(path: NodePath): NodeMutation[] {
if ((path.isBinaryExpression() || path.isLogicalExpression()) && this.hasValidParent(path) && this.isValidOperator(path.node.operator)) {
return [
{ original: path.node, replacement: types.booleanLiteral(true) },
{ original: path.node, replacement: types.booleanLiteral(false) },
];
} else if (path.isDoWhileStatement() || path.isWhileStatement()) {
return [{ original: path.node.test, replacement: types.booleanLiteral(false) }];
} else if (path.isForStatement()) {
if (!path.node.test) {
const replacement = types.cloneNode(path.node, /* deep */ false);
replacement.test = types.booleanLiteral(false);
return [{ original: path.node, replacement }];
} else {
return [{ original: path.node.test, replacement: types.booleanLiteral(false) }];
}
} else if (path.isIfStatement()) {
return [
// raw string mutations in the `if` condition
{ original: path.node.test, replacement: types.booleanLiteral(true) },
{ original: path.node.test, replacement: types.booleanLiteral(false) },
];
} else if (
path.isSwitchCase() &&
// if not a fallthrough case
path.node.consequent.length > 0
) {
const replacement = types.cloneNode(path.node);
replacement.consequent = [];
return [{ original: path.node, replacement }];
}

return [];
}
}
21 changes: 21 additions & 0 deletions packages/instrumenter/src/mutators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NodePath } from '@babel/core';
import { flatMap } from '@stryker-mutator/util';

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

import { ArithmeticOperatorMutator } from './arithmetic-operator-mutator';
import { NodeMutator } from './node-mutator';
import { BlockStatementMutator } from './block-statement-mutator';
import { ConditionalExpressionMutator } from './conditional-expression-mutator';
import { StringLiteralMutator } from './string-literal-mutator';

export * from './node-mutator';
export const mutators: NodeMutator[] = [
new ArithmeticOperatorMutator(),
new BlockStatementMutator(),
new ConditionalExpressionMutator(),
new StringLiteralMutator(),
];
export const mutate = (node: NodePath): NamedNodeMutation[] => {
return flatMap(mutators, (mutator) => mutator.mutate(node).map((nodeMutation) => ({ ...nodeMutation, mutatorName: mutator.name })));
};
8 changes: 8 additions & 0 deletions packages/instrumenter/src/mutators/node-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NodePath } from '@babel/core';

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

export interface NodeMutator {
mutate(path: NodePath): NodeMutation[];
readonly name: string;
}
42 changes: 42 additions & 0 deletions packages/instrumenter/src/mutators/string-literal-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { types, NodePath } from '@babel/core';

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

import { NodeMutator } from './node-mutator';

export class StringLiteralMutator implements NodeMutator {
public name = 'StringLiteral';

public mutate(path: NodePath): NodeMutation[] {
if (path.isTemplateLiteral()) {
if (path.node.quasis.length === 1 && path.node.quasis[0].value.raw.length === 0) {
return [
{
original: path.node,
replacement: types.templateLiteral([types.templateElement({ raw: 'Stryker was here!' })], []),
},
];
} else {
return [
{
original: path.node,
replacement: types.templateLiteral([types.templateElement({ raw: '' })], []),
},
];
}
} else if (this.isDeclarationOrJSX(path.parent) && path.isStringLiteral()) {
return [
{
original: path.node,
replacement: types.stringLiteral(path.node.value.length === 0 ? 'Stryker was here!' : ''),
},
];
} else {
return [];
}
}

private isDeclarationOrJSX(parent?: types.Node): boolean {
return !types.isImportDeclaration(parent) && !types.isExportDeclaration(parent) && !types.isJSXAttribute(parent);
}
}
Loading

0 comments on commit 3b57ef2

Please sign in to comment.