-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(regex-mutator): smart regex mutations (#2709)
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
Showing
6 changed files
with
121 additions
and
2 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
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,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 []; | ||
} | ||
} |
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 |
---|---|---|
|
@@ -16,5 +16,6 @@ | |
{ | ||
"path": "../../util/tsconfig.src.json" | ||
} | ||
] | ||
], | ||
"include": ["**/*.*", "../typings/*.d.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,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
40
packages/instrumenter/test/unit/mutators/regex-mutator.spec.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,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}")'); | ||
}); | ||
}); |