diff --git a/packages/instrumenter/package.json b/packages/instrumenter/package.json index 8f78b814bc..beec10219f 100644 --- a/packages/instrumenter/package.json +++ b/packages/instrumenter/package.json @@ -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", diff --git a/packages/instrumenter/src/mutators/index.ts b/packages/instrumenter/src/mutators/index.ts index f62a043848..85b9d5c0f5 100644 --- a/packages/instrumenter/src/mutators/index.ts +++ b/packages/instrumenter/src/mutators/index.ts @@ -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'; @@ -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) => diff --git a/packages/instrumenter/src/mutators/regex-mutator.ts b/packages/instrumenter/src/mutators/regex-mutator.ts new file mode 100644 index 0000000000..00f1fa5fae --- /dev/null +++ b/packages/instrumenter/src/mutators/regex-mutator.ts @@ -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) { + 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 []; + } +} diff --git a/packages/instrumenter/src/tsconfig.json b/packages/instrumenter/src/tsconfig.json index 4022aebfe2..cb9df01984 100644 --- a/packages/instrumenter/src/tsconfig.json +++ b/packages/instrumenter/src/tsconfig.json @@ -16,5 +16,6 @@ { "path": "../../util/tsconfig.src.json" } - ] + ], + "include": ["**/*.*", "../typings/*.d.ts"] } diff --git a/packages/instrumenter/src/typings/weapon-regex.ts b/packages/instrumenter/src/typings/weapon-regex.ts new file mode 100644 index 0000000000..bfed53020a --- /dev/null +++ b/packages/instrumenter/src/typings/weapon-regex.ts @@ -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[]; +} diff --git a/packages/instrumenter/test/unit/mutators/regex-mutator.spec.ts b/packages/instrumenter/test/unit/mutators/regex-mutator.spec.ts new file mode 100644 index 0000000000..231537ebad --- /dev/null +++ b/packages/instrumenter/test/unit/mutators/regex-mutator.spec.ts @@ -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}")'); + }); +});