Skip to content

Commit

Permalink
feat(regex-mutator): smart regex mutations (#2709)
Browse files Browse the repository at this point in the history
Add support for smart regex mutations to Stryker. 👽

Some examples:

```ts
/\d{4}\s[a-Z]{2}/;
// Mutates to:
/\d\s[a-Z]{2}/; // => Quantifier removal
/\D{4}\s[a-Z]{2}/; // => Predefined Character Class Negation
/\d{4}\S[a-Z]{2}/; // => Predefined Character Class Negation
/\d{4}\s[a-Z]/; // => Quantifier removal
/\d{4}\s[^a-Z]{2}/; // => Character Class Negation
```

Stryker will identify regex literals: `/\d{4}\s[a-Z]{2}/` as well as clear regex string literals in a `RegExp` constructor: `new RegExp('\\d{4}\\s[a-Z]{2}')`. After that it uses the awesome [Weapon-regeX](https://github.com/stryker-mutator/weapon-regex/) library to generate mutants based on the regex pattern. 

Weapon regex supports mutation levels. Currently, Stryker only introduces regex mutants of mutation level 1 in your code. We might be making this configurable in the future.

You can opt-out of using this mutator by excluding it via stryker.conf.json:

```json
{
  "mutator": {
    "excludedMutations": ["Regex"]
  }
}
```
  • Loading branch information
nicojs authored Jan 24, 2021
1 parent 1070eca commit 0877f44
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 2 deletions.
3 changes: 2 additions & 1 deletion packages/instrumenter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"@babel/preset-typescript": "~7.12.1 ",
"@stryker-mutator/api": "4.3.1",
"@stryker-mutator/util": "4.3.1",
"angular-html-parser": "~1.7.0"
"angular-html-parser": "~1.7.0",
"weapon-regex": "~0.3.0"
},
"devDependencies": {
"@babel/preset-react": "~7.12.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/instrumenter/src/mutators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ObjectLiteralMutator } from './object-literal-mutator';
import { UnaryOperatorMutator } from './unary-operator-mutator';
import { UpdateOperatorMutator } from './update-operator-mutator';
import { MutatorOptions } from './mutator-options';
import { RegexMutator } from './regex-mutator';

export * from './node-mutator';
export * from './mutator-options';
Expand All @@ -34,6 +35,7 @@ export const mutators: NodeMutator[] = [
new StringLiteralMutator(),
new UnaryOperatorMutator(),
new UpdateOperatorMutator(),
new RegexMutator(),
];
export const mutate = (node: NodePath, { excludedMutations }: MutatorOptions): NamedNodeMutation[] => {
return flatMap(mutators, (mutator) =>
Expand Down
63 changes: 63 additions & 0 deletions packages/instrumenter/src/mutators/regex-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as types from '@babel/types';
import { NodePath } from '@babel/core';
import * as weaponRegex from 'weapon-regex';

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

import { NodeMutator } from '.';

/**
* Checks that a string literal is an obvious regex string literal
* @param path The string literal to checks
* @example
* new RegExp("\\d{4}");
*/
function isObviousRegexString(path: NodePath<types.StringLiteral>) {
return (
path.parentPath.isNewExpression() &&
types.isIdentifier(path.parentPath.node.callee) &&
path.parentPath.node.callee.name === RegExp.name &&
path.parentPath.node.arguments[0] === path.node
);
}
const weaponRegexOptions: weaponRegex.Options = { mutationLevels: [1] };

function mutatePattern(pattern: string): string[] {
if (pattern.length) {
try {
return weaponRegex.mutate(pattern, weaponRegexOptions).map((mutant) => mutant.pattern);
} catch (err) {
console.error(
`[RegexMutator]: The Regex parser of weapon-regex couldn't parse this regex pattern: "${pattern}". Please report this issue at https://github.com/stryker-mutator/weapon-regex/issues. Inner error: ${err.message}`
);
}
}
return [];
}

export class RegexMutator implements NodeMutator {
public name = 'Regex';

public mutate(path: NodePath): NodeMutation[] {
if (path.isRegExpLiteral()) {
return mutatePattern(path.node.pattern).map((replacementPattern) => {
const replacement = types.cloneNode(path.node, false);
replacement.pattern = replacementPattern;
return {
original: path.node,
replacement,
};
});
} else if (path.isStringLiteral() && isObviousRegexString(path)) {
return mutatePattern(path.node.value).map((replacementPattern) => {
const replacement = types.cloneNode(path.node, false);
replacement.value = replacementPattern;
return {
original: path.node,
replacement,
};
});
}
return [];
}
}
3 changes: 2 additions & 1 deletion packages/instrumenter/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
{
"path": "../../util/tsconfig.src.json"
}
]
],
"include": ["**/*.*", "../typings/*.d.ts"]
}
12 changes: 12 additions & 0 deletions packages/instrumenter/src/typings/weapon-regex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare module 'weapon-regex' {
export interface Options {
mutationLevels: number[];
}

export interface Mutant {
description: string;
pattern: string;
}

export function mutate(pattern: string, ops?: Options): Mutant[];
}
40 changes: 40 additions & 0 deletions packages/instrumenter/test/unit/mutators/regex-mutator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from 'chai';
import sinon from 'sinon';

import { RegexMutator } from '../../../src/mutators/regex-mutator';
import { expectJSMutation } from '../../helpers/expect-mutation';

describe(RegexMutator.name, () => {
let sut: RegexMutator;
beforeEach(() => {
sut = new RegexMutator();
});

it('should have name "Regex"', () => {
expect(sut.name).eq('Regex');
});

it('should not mutate normal string literals', () => {
expectJSMutation(sut, '""');
});

it('should mutate a regex literal', () => {
expectJSMutation(sut, '/\\d{4}/', '/\\d/', '/\\D{4}/');
});

it("should not crash if a regex couldn't be parsed", () => {
const errorStub = sinon.stub(console, 'error');
expectJSMutation(sut, '/[[]]/');
expect(errorStub).calledWith(
'[RegexMutator]: The Regex parser of weapon-regex couldn\'t parse this regex pattern: "[[]]". Please report this issue at https://github.com/stryker-mutator/weapon-regex/issues. Inner error: [Error] Parser: Position 1:1, found "[[]]"'
);
});

it('should mutate obvious Regex string literals', () => {
expectJSMutation(sut, 'new RegExp("\\\\d{4}")', 'new RegExp("\\\\d")', 'new RegExp("\\\\D{4}")');
});

it('should not mutate the flags of a new RegExp constructor', () => {
expectJSMutation(sut, 'new RegExp("", "\\\\d{4}")');
});
});

0 comments on commit 0877f44

Please sign in to comment.