diff --git a/packages/instrumenter/src/mutant-placers/expression-mutant-placer.ts b/packages/instrumenter/src/mutant-placers/expression-mutant-placer.ts index 0bb89d2653..7b500e6171 100644 --- a/packages/instrumenter/src/mutant-placers/expression-mutant-placer.ts +++ b/packages/instrumenter/src/mutant-placers/expression-mutant-placer.ts @@ -78,7 +78,7 @@ function isCallExpression(path: NodePath): path is NodePath) { const parent = path.parentPath; - return !isObjectPropertyKey() && !isPartOfChain() && !parent.isTaggedTemplateExpression(); + return !isObjectPropertyKey() && !isPartOfChain() && !parent.isTaggedTemplateExpression() && !isPartOfDeleteExpression(); /** * Determines if the expression is property of an object. @@ -112,12 +112,22 @@ function isValidExpression(path: NodePath) { (isCallExpression(parent) && parent.node.callee === path.node)) ); } + + /** + * Determines if the expression is part of a delete expression. + * @returns true if the expression is part of a delete expression + * @example + * delete foo.bar; + */ + function isPartOfDeleteExpression() { + return parent.isUnaryExpression() && parent.node.operator === 'delete'; + } } /** * Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : originalCode`; */ -export const expressionMutantPlacer: MutantPlacer = { +export const expressionMutantPlacer = { name: 'expressionMutantPlacer', canPlace(path) { return path.isExpression() && isValidExpression(path); @@ -135,4 +145,4 @@ export const expressionMutantPlacer: MutantPlacer = { } path.replaceWith(expression); }, -}; +} satisfies MutantPlacer; diff --git a/packages/instrumenter/test/unit/mutant-placers/expression-mutant-placer.spec.ts b/packages/instrumenter/test/unit/mutant-placers/expression-mutant-placer.spec.ts index e4638eec9f..3da26797f0 100644 --- a/packages/instrumenter/test/unit/mutant-placers/expression-mutant-placer.spec.ts +++ b/packages/instrumenter/test/unit/mutant-placers/expression-mutant-placer.spec.ts @@ -35,6 +35,20 @@ describe('expressionMutantPlacer', () => { expect(expressionMutantPlacer.canPlace(templateLiteral)).false; }); + it('should be false when the parent is a delete unary expression', () => { + const memberExpression = findNodePath(parseJS('delete myVariable?.[indexer];'), (p) => p.isOptionalMemberExpression()); + expect(expressionMutantPlacer.canPlace(memberExpression)).false; + }); + + it('should be true when the parent is a non-delete unary expression', () => { + const memberExpression = findNodePath(parseJS('void myVariable[indexer];'), (p) => p.isMemberExpression()); + const memberExpression2 = findNodePath(parseJS('typeof myVariable[indexer];'), (p) => p.isMemberExpression()); + const memberExpression3 = findNodePath(parseJS('throw myVariable[indexer];'), (p) => p.isMemberExpression()); + expect(expressionMutantPlacer.canPlace(memberExpression)).true; + expect(expressionMutantPlacer.canPlace(memberExpression2)).true; + expect(expressionMutantPlacer.canPlace(memberExpression3)).true; + }); + describe('object literals', () => { it('should be false when the expression is a key', () => { // A stringLiteral is considered an expression, while it is not save to place a mutant there! diff --git a/packages/instrumenter/testResources/instrumenter/optional-chains.ts b/packages/instrumenter/testResources/instrumenter/optional-chains.ts index efb4829992..b1378f8264 100644 --- a/packages/instrumenter/testResources/instrumenter/optional-chains.ts +++ b/packages/instrumenter/testResources/instrumenter/optional-chains.ts @@ -11,4 +11,8 @@ input?.id!.toString(); bar?.baz[0] -state.stats[organization?.organization_id] = action.payload.stats; \ No newline at end of file +state.stats[organization?.organization_id] = action.payload.stats; + +// https://github.com/stryker-mutator/stryker-js/issues/4741 +delete myVariable?.[indexer]; +void foo?.(); diff --git a/packages/instrumenter/testResources/instrumenter/optional-chains.ts.out.snap b/packages/instrumenter/testResources/instrumenter/optional-chains.ts.out.snap index aa18c9ab69..80443f64e7 100644 --- a/packages/instrumenter/testResources/instrumenter/optional-chains.ts.out.snap +++ b/packages/instrumenter/testResources/instrumenter/optional-chains.ts.out.snap @@ -53,5 +53,9 @@ const directiveRanges = stryMutAct_9fa48(\\"6\\") ? comments.map(tryParseTSDirec const qux = quux(stryMutAct_9fa48(\\"7\\") ? corge.cov() : (stryCov_9fa48(\\"7\\"), corge?.cov())); stryMutAct_9fa48(\\"8\\") ? input.id!.toString() : (stryCov_9fa48(\\"8\\"), input?.id!.toString()); stryMutAct_9fa48(\\"9\\") ? bar.baz[0] : (stryCov_9fa48(\\"9\\"), bar?.baz[0]); -state.stats[stryMutAct_9fa48(\\"10\\") ? organization.organization_id : (stryCov_9fa48(\\"10\\"), organization?.organization_id)] = action.payload.stats;" +state.stats[stryMutAct_9fa48(\\"10\\") ? organization.organization_id : (stryCov_9fa48(\\"10\\"), organization?.organization_id)] = action.payload.stats; + +// https://github.com/stryker-mutator/stryker-js/issues/4741 +stryMutAct_9fa48(\\"11\\") ? delete myVariable[indexer] : (stryCov_9fa48(\\"11\\"), delete myVariable?.[indexer]); +void (stryMutAct_9fa48(\\"12\\") ? foo() : (stryCov_9fa48(\\"12\\"), foo?.()));" `;