@systemfsoftware/stryker-js-instrumenter 8.0.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @systemfsoftware/stryker-js-instrumenter
2
2
 
3
+ ## 8.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Three opt-in mutators plant Effect concurrency faults on top of the default set, so a mutation run can check that your concurrency tests catch races. `AtomicUpdateSplit` splits read-modify-write calls on `Ref` and `SynchronizedRef` into a separate read, a yield, and a separate write. `SynchronizationRemoval` removes semaphore permits, `Effect.uninterruptible`, and `Effect.uninterruptibleMask`. `FinalizerEscape` stops `ensuring`, `onExit`, `onError`, `onInterrupt`, `acquireRelease`, and `acquireUseRelease` cleanup from running on interruption. All three are off by default. Enable any subset by listing them under `mutator: { optInMutations: [...] }` in your configuration. A name that is not an opt-in mutator fails the run, and the error lists the known names. The replacements use Effect 4 APIs, so a repository on Effect 3.x must not opt in.
8
+
9
+ ### Patch Changes
10
+
11
+ - Disable comments with the `next-line` scope now ignore the mutants on the line directly below the comment and record the given reason. Before this fix they ignored nothing.
12
+
3
13
  ## 8.0.2
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -16,6 +16,24 @@ pnpm add @systemfsoftware/stryker-js-instrumenter
16
16
 
17
17
  The instrumenter is used internally by the Stryker mutation testing framework to instrument source files with mutant coverage and switching logic.
18
18
 
19
+ ## Opt-in mutators
20
+
21
+ Three mutators that plant Effect concurrency faults are available on request and stay out of the default set. Enable any subset in your Stryker config:
22
+
23
+ ```json
24
+ {
25
+ "mutator": {
26
+ "optInMutations": ["AtomicUpdateSplit", "SynchronizationRemoval", "FinalizerEscape"]
27
+ }
28
+ }
29
+ ```
30
+
31
+ - **AtomicUpdateSplit** turns read-modify-write calls such as `Ref.update` or `SynchronizedRef.modify` into a separate read, a yield, and a separate write, so an interleaved mutation can lose an update.
32
+ - **SynchronizationRemoval** drops the guarding effect of `Semaphore.withPermits`/`withPermit` and `Effect.uninterruptible`, letting code that assumed exclusivity or uninterruptibility run without it.
33
+ - **FinalizerEscape** weakens `Effect.ensuring`, `Effect.onExit`, `Effect.onError`, and `Effect.onInterrupt` so their cleanup no longer runs on interruption.
34
+
35
+ Naming a mutator that does not exist fails the run and lists the names that do. The replacements these mutators plant use Effect 4 APIs, so a repository on Effect 3.x must not opt in.
36
+
19
37
  ## License
20
38
 
21
39
  Apache-2.0. Part of [systemfsoftware](https://github.com/systemfsoftware/stryker-js-effect/tree/main/packages/stryker-js-instrumenter#readme).
package/dist/index.d.mts CHANGED
@@ -71,6 +71,7 @@ export declare const InstrumenterOptionsSchema: S.Struct<{
71
71
  readonly excludedMutations: S.$Array<S.String>;
72
72
  readonly ignorers: S.$Array<S.Unknown>;
73
73
  readonly noHeader: S.optional<S.Boolean>;
74
+ readonly optInMutations: S.optional<S.$Array<S.String>>;
74
75
  }>;
75
76
 
76
77
  export declare class InstrumentError extends InstrumentError_base {
package/dist/index.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  import * as Effect from "effect/Effect";
2
+ import * as Option from "effect/Option";
2
3
  import * as Predicate from "effect/Predicate";
3
4
  import * as S from "effect/Schema";
4
5
  import * as Arr from "effect/Array";
5
6
  import * as Match from "effect/Match";
6
- import * as Option from "effect/Option";
7
7
  import { walk } from "oxc-walker";
8
8
  import { Schema } from "effect";
9
9
  import { RegExpParser, visitRegExpAST } from "@eslint-community/regexpp";
10
+ import * as Bool from "effect/Boolean";
10
11
  //#region src/Location.schema.ts
11
12
  const PositionSchema = S.Struct({
12
13
  line: S.Finite,
@@ -88,7 +89,8 @@ const IgnorerSchema = S.Unknown;
88
89
  const InstrumenterOptionsSchema = S.Struct({
89
90
  excludedMutations: S.Array(S.String),
90
91
  ignorers: S.Array(IgnorerSchema),
91
- noHeader: S.optional(S.Boolean)
92
+ noHeader: S.optional(S.Boolean),
93
+ optInMutations: S.optional(S.Array(S.String))
92
94
  });
93
95
  var InstrumentResult = class extends S.TaggedClass()("InstrumentResult", {
94
96
  files: S.Array(FileSchema),
@@ -403,6 +405,22 @@ function unaryExpression(operator, argument, loc) {
403
405
  prefix: true
404
406
  }, loc);
405
407
  }
408
+ function binaryExpression(operator, left, right, loc) {
409
+ return mark({
410
+ type: "BinaryExpression",
411
+ operator,
412
+ left,
413
+ right
414
+ }, loc);
415
+ }
416
+ function logicalExpression(operator, left, right, loc) {
417
+ return mark({
418
+ type: "LogicalExpression",
419
+ operator,
420
+ left,
421
+ right
422
+ }, loc);
423
+ }
406
424
  function updateExpression(operator, argument, prefix, loc) {
407
425
  return mark({
408
426
  type: "UpdateExpression",
@@ -3028,14 +3046,14 @@ function isTSType(node) {
3028
3046
  function isNodeOfKind(node, kind) {
3029
3047
  return node.type === kind;
3030
3048
  }
3031
- const FUNCTION_KINDS = {
3049
+ const FUNCTION_KINDS$1 = {
3032
3050
  FunctionDeclaration: true,
3033
3051
  FunctionExpression: true,
3034
3052
  TSDeclareFunction: true,
3035
3053
  TSEmptyBodyFunctionExpression: true
3036
3054
  };
3037
3055
  function isFunctionNode(node) {
3038
- return FUNCTION_KINDS[node.type] === true;
3056
+ return FUNCTION_KINDS$1[node.type] === true;
3039
3057
  }
3040
3058
  function isAssignmentPattern(node) {
3041
3059
  return node.type === "AssignmentPattern";
@@ -3556,6 +3574,591 @@ function objectCauseTextOf(cause, depth) {
3556
3574
  return isObjectType(cause) ? textWithNested(ownCauseText(cause), causeText(fieldOf(cause, "cause"), depth + 1)) : void 0;
3557
3575
  }
3558
3576
  //#endregion
3577
+ //#region src/EffectCall.ts
3578
+ /**
3579
+ * EffectCall — the resolution front half the opt-in concurrency mutators share.
3580
+ *
3581
+ * Given a visited node it answers: is this a covered Effect call (R1); through
3582
+ * which module and export did its callee resolve; which call form does its
3583
+ * argument count select (KTD10); does a data-last form sit where the replacement
3584
+ * function is contextually typed (R4); and how can each `effect` module the
3585
+ * replacement needs be reached from the file's own imports (KTD3). It decides
3586
+ * nothing about a replacement: a mutator asks for module access through the
3587
+ * description's `moduleAccess`, and gets none when the file has no unshadowed
3588
+ * binding for that module (R2).
3589
+ *
3590
+ * Pure. One expression per decision, no I/O, no throwing. The import table is
3591
+ * built once per Program and memoised in a WeakMap, because every visited node
3592
+ * asks for it and a file's imports never change mid-parse (KTD4).
3593
+ */
3594
+ const MODULE_NAMES = [
3595
+ "Effect",
3596
+ "Ref",
3597
+ "Semaphore",
3598
+ "SynchronizedRef"
3599
+ ];
3600
+ const EFFECT_SOURCE = "effect";
3601
+ const FUNCTION_SOURCE = "effect/Function";
3602
+ const DATA_FIRST = "data-first";
3603
+ const DATA_LAST_PIPE_ARG = "data-last-pipe-arg";
3604
+ const DATA_LAST_PIPE_METHOD = "data-last-pipe-method";
3605
+ const DATA_LAST_IMMEDIATE = "data-last-immediate";
3606
+ const PIPED_REFERENCE = "piped-reference";
3607
+ const NO_ARGUMENTS = Object.freeze([]);
3608
+ const ARITIES = Object.freeze({
3609
+ "Ref.modify": {
3610
+ dataFirst: [2],
3611
+ dataLast: 1
3612
+ },
3613
+ "Ref.modifySome": {
3614
+ dataFirst: [2],
3615
+ dataLast: 1
3616
+ },
3617
+ "Ref.update": {
3618
+ dataFirst: [2],
3619
+ dataLast: 1
3620
+ },
3621
+ "Ref.updateSome": {
3622
+ dataFirst: [2],
3623
+ dataLast: 1
3624
+ },
3625
+ "Ref.updateAndGet": {
3626
+ dataFirst: [2],
3627
+ dataLast: 1
3628
+ },
3629
+ "Ref.getAndUpdate": {
3630
+ dataFirst: [2],
3631
+ dataLast: 1
3632
+ },
3633
+ "SynchronizedRef.modify": {
3634
+ dataFirst: [2],
3635
+ dataLast: 1
3636
+ },
3637
+ "SynchronizedRef.modifySome": {
3638
+ dataFirst: [2],
3639
+ dataLast: 1
3640
+ },
3641
+ "SynchronizedRef.update": {
3642
+ dataFirst: [2],
3643
+ dataLast: 1
3644
+ },
3645
+ "SynchronizedRef.updateSome": {
3646
+ dataFirst: [2],
3647
+ dataLast: 1
3648
+ },
3649
+ "SynchronizedRef.updateAndGet": {
3650
+ dataFirst: [2],
3651
+ dataLast: 1
3652
+ },
3653
+ "SynchronizedRef.getAndUpdate": {
3654
+ dataFirst: [2],
3655
+ dataLast: 1
3656
+ },
3657
+ "Semaphore.withPermits": {
3658
+ dataFirst: [3],
3659
+ dataLast: 2
3660
+ },
3661
+ "Semaphore.withPermit": {
3662
+ dataFirst: [2],
3663
+ dataLast: 1
3664
+ },
3665
+ "Effect.ensuring": {
3666
+ dataFirst: [2],
3667
+ dataLast: 1
3668
+ },
3669
+ "Effect.onExit": {
3670
+ dataFirst: [2],
3671
+ dataLast: 1
3672
+ },
3673
+ "Effect.onError": {
3674
+ dataFirst: [2],
3675
+ dataLast: 1
3676
+ },
3677
+ "Effect.onInterrupt": {
3678
+ dataFirst: [2],
3679
+ dataLast: 1
3680
+ },
3681
+ "Effect.uninterruptible": { dataFirst: [1] },
3682
+ "Effect.uninterruptibleMask": { dataFirst: [1] },
3683
+ "Effect.acquireRelease": { dataFirst: [2, 3] },
3684
+ "Effect.acquireUseRelease": { dataFirst: [3] }
3685
+ });
3686
+ const EMPTY_IMPORT_TABLE = Object.freeze({
3687
+ moduleBindings: /* @__PURE__ */ new Map(),
3688
+ namespaces: /* @__PURE__ */ new Map(),
3689
+ bareFunctions: /* @__PURE__ */ new Map(),
3690
+ pipeBindings: /* @__PURE__ */ new Set()
3691
+ });
3692
+ const importTables = /* @__PURE__ */ new WeakMap();
3693
+ const importTableFor = (program) => Option.getOrElse(Option.fromNullishOr(importTables.get(program)), () => rememberImportTable(program));
3694
+ function rememberImportTable(program) {
3695
+ const table = buildImportTable(program);
3696
+ importTables.set(program, table);
3697
+ return table;
3698
+ }
3699
+ const buildImportTable = (program) => Arr.reduce(Arr.flatMap(Arr.filter(program.body, isImportDeclaration$1), importContributions), EMPTY_IMPORT_TABLE, applyContribution);
3700
+ const withEntry = (entries, key, value) => new Map([...entries, [key, value]]);
3701
+ const withMember = (members, value) => /* @__PURE__ */ new Set([...members, value]);
3702
+ const applyContribution = (table, contribution) => Match.value(contribution).pipe(Match.when((candidate) => candidate.kind === "module", (moduleBinding) => ({
3703
+ ...table,
3704
+ moduleBindings: withEntry(table.moduleBindings, moduleBinding.local, moduleBinding.module)
3705
+ })), Match.when((candidate) => candidate.kind === "namespace", (namespaceBinding) => ({
3706
+ ...table,
3707
+ namespaces: withEntry(table.namespaces, namespaceBinding.local, namespaceBinding.binding)
3708
+ })), Match.when((candidate) => candidate.kind === "bareFunction", (bareFunction) => ({
3709
+ ...table,
3710
+ bareFunctions: withEntry(table.bareFunctions, bareFunction.local, {
3711
+ module: bareFunction.module,
3712
+ exportName: bareFunction.exportName
3713
+ })
3714
+ })), Match.orElse((pipeBinding) => ({
3715
+ ...table,
3716
+ pipeBindings: withMember(table.pipeBindings, pipeBinding.local)
3717
+ })));
3718
+ const importContributions = (declaration) => Option.toArray(onlyWhen(declaration.importKind !== "type", declaration)).flatMap((imported) => Arr.flatMap(imported.specifiers, (specifier) => Option.toArray(specifierContribution(imported.source.value, specifier))));
3719
+ const specifierContribution = (source, specifier) => Match.value(specifier).pipe(Match.when(isNamedImportSpecifier, (named) => namedSpecifierContribution(source, named)), Match.when(isNamespaceImportSpecifier, (namespace) => namespaceSpecifierContribution(source, namespace)), Match.orElse(() => Option.none()));
3720
+ const namedSpecifierContribution = (source, specifier) => onlyWhen(specifier.importKind !== "type", specifier).pipe(Option.flatMap((imported) => namedContribution(source, moduleExportName(imported.imported), imported.local.name)));
3721
+ const namedContribution = (source, importedName, localName) => Match.value(source).pipe(Match.when((candidate) => candidate === EFFECT_SOURCE, () => effectRootContribution(importedName, localName)), Match.when((candidate) => candidate === FUNCTION_SOURCE, () => pipeContribution(importedName, localName)), Match.orElse(() => Option.map(moduleNameFromSource(source), (module) => bareFunctionContribution(localName, module, importedName))));
3722
+ const effectRootContribution = (importedName, localName) => Option.orElse(Option.map(moduleNameFromName(importedName), (module) => moduleContribution(localName, module)), () => pipeContribution(importedName, localName));
3723
+ const pipeContribution = (importedName, localName) => onlyWhen(importedName === "pipe", pipeContributionOf(localName));
3724
+ const namespaceSpecifierContribution = (source, specifier) => Option.map(namespaceBindingFor(source), (binding) => ({
3725
+ kind: "namespace",
3726
+ local: specifier.local.name,
3727
+ binding
3728
+ }));
3729
+ const moduleContribution = (local, module) => ({
3730
+ kind: "module",
3731
+ local,
3732
+ module
3733
+ });
3734
+ const bareFunctionContribution = (local, module, exportName) => ({
3735
+ kind: "bareFunction",
3736
+ local,
3737
+ module,
3738
+ exportName
3739
+ });
3740
+ const pipeContributionOf = (local) => ({
3741
+ kind: "pipe",
3742
+ local
3743
+ });
3744
+ const namespaceBindingFor = (source) => Match.value(source).pipe(Match.when((candidate) => candidate === EFFECT_SOURCE, () => Option.some({ kind: "root" })), Match.when((candidate) => candidate === FUNCTION_SOURCE, () => Option.some({ kind: "functionModule" })), Match.orElse(() => Option.map(moduleNameFromSource(source), (module) => ({
3745
+ kind: "module",
3746
+ module
3747
+ }))));
3748
+ const moduleNameFromName = (name) => Arr.findFirst(MODULE_NAMES, (module) => module === name);
3749
+ const moduleNameFromSource = (source) => Arr.findFirst(MODULE_NAMES, (module) => source === `${EFFECT_SOURCE}/${module}`);
3750
+ const resolveEffectCall = (node, context) => Option.flatMap(programOf(context), (program) => resolveIn(node, context, importTableFor(program)));
3751
+ const programOf = (context) => Arr.findLast(context.ancestors, isProgram);
3752
+ const resolveIn = (node, context, table) => Match.value(node).pipe(Match.when(isCallExpression$1, (call) => resolveCall(call, context, table)), Match.when(isMemberExpression, (member) => resolveReference(member, context, table)), Match.when(isIdentifier$1, (reference) => resolveReference(reference, context, table)), Match.orElse(() => Option.none()));
3753
+ const resolveCall = (call, context, table) => Option.flatMap(resolveCallee(call.callee, context, table), (callee) => Option.flatMap(spreadFreeArguments$1(call.arguments), (args) => Option.flatMap(callForm(callee, args, call, context, table), (form) => Option.some({
3754
+ module: callee.module,
3755
+ exportName: callee.exportName,
3756
+ form,
3757
+ args,
3758
+ moduleAccess: moduleAccessFunction(callee, context, table)
3759
+ }))));
3760
+ const resolveReference = (reference, context, table) => Option.flatMap(resolveCallee(reference, context, table), (callee) => Option.flatMap(referenceForm(reference, context, table), (form) => Option.some({
3761
+ module: callee.module,
3762
+ exportName: callee.exportName,
3763
+ form,
3764
+ args: NO_ARGUMENTS,
3765
+ moduleAccess: moduleAccessFunction(callee, context, table)
3766
+ })));
3767
+ const referenceForm = (reference, context, table) => Option.flatMap(parentCall(context), (parent) => Option.map(pipePosition(parent, reference, context, table), () => PIPED_REFERENCE));
3768
+ const spreadFreeArguments$1 = (args) => Match.value(args.some(isSpreadElement)).pipe(Match.when(true, () => Option.none()), Match.orElse(() => Option.some(args.filter(isSpreadFree))));
3769
+ const resolveCallee = (callee, context, table) => Match.value(callee).pipe(Match.when(isIdentifier$1, (reference) => resolveBareCallee(reference, context, table)), Match.when(isMemberExpression, (member) => resolveMemberCallee(member, context, table)), Match.orElse(() => Option.none()));
3770
+ const resolveBareCallee = (reference, context, table) => Option.flatMap(Option.fromNullishOr(table.bareFunctions.get(reference.name)), (binding) => Option.map(unshadowedName(reference.name, context), () => ({
3771
+ module: binding.module,
3772
+ exportName: binding.exportName,
3773
+ owner: Option.none()
3774
+ })));
3775
+ const resolveMemberCallee = (member, context, table) => Option.flatMap(staticMemberName(member), (exportName) => Option.map(resolveModuleObject(member.object, context, table), (owner) => ({
3776
+ module: owner.module,
3777
+ exportName,
3778
+ owner: Option.some(owner)
3779
+ })));
3780
+ const resolveModuleObject = (expression, context, table) => Match.value(expression).pipe(Match.when(isIdentifier$1, (reference) => identifierModuleObject(reference, context, table)), Match.when(isMemberExpression, (member) => namespacedModuleObject(member, context, table)), Match.orElse(() => Option.none()));
3781
+ const identifierModuleObject = (reference, context, table) => Option.orElse(Option.flatMap(Option.fromNullishOr(table.moduleBindings.get(reference.name)), (module) => Option.map(unshadowedName(reference.name, context), () => ({
3782
+ module,
3783
+ access: reference
3784
+ }))), () => Option.flatMap(namespaceOf(reference, context, table), (binding) => Option.map(moduleOf(binding), (module) => ({
3785
+ module,
3786
+ access: reference
3787
+ }))));
3788
+ const namespacedModuleObject = (member, context, table) => Option.flatMap(staticMemberName(member), (moduleName) => Option.flatMap(namespaceOf(member.object, context, table), (binding) => Match.value(binding).pipe(Match.when(isRootNamespace, () => Option.map(moduleNameFromName(moduleName), (module) => ({
3789
+ module,
3790
+ access: member
3791
+ }))), Match.when(isModuleNamespace, (moduleNamespace) => Option.map(onlyWhen(moduleNamespace.module === moduleName, member), (access) => ({
3792
+ module: moduleNamespace.module,
3793
+ access
3794
+ }))), Match.orElse(() => Option.none()))));
3795
+ const isRootNamespace = (binding) => binding.kind === "root";
3796
+ const moduleOf = (binding) => Match.value(binding).pipe(Match.when(isModuleNamespace, (moduleNamespace) => Option.some(moduleNamespace.module)), Match.orElse(() => Option.none()));
3797
+ const isModuleNamespace = (binding) => binding.kind === "module";
3798
+ const namespaceOf = (expression, context, table) => Match.value(expression).pipe(Match.when(isIdentifier$1, (reference) => Option.flatMap(Option.fromNullishOr(table.namespaces.get(reference.name)), (binding) => Option.map(unshadowedName(reference.name, context), () => binding))), Match.orElse(() => Option.none()));
3799
+ const callForm = (callee, args, call, context, table) => Option.flatMap(arityOf(callee), (arity) => Match.value(args.length).pipe(Match.when((count) => arity.dataFirst.includes(count), () => Option.some(DATA_FIRST)), Match.when((count) => arity.dataLast === count, () => dataLastForm(call, context, table)), Match.orElse(() => Option.none())));
3800
+ const arityOf = (callee) => Option.fromNullishOr(ARITIES[`${callee.module}.${callee.exportName}`]);
3801
+ const dataLastForm = (call, context, table) => Option.flatMap(parentCall(context), (parent) => Match.value(parent.callee === call).pipe(Match.when(true, () => Option.some(DATA_LAST_IMMEDIATE)), Match.orElse(() => Option.flatMap(pipePosition(parent, call, context, table), formForPosition))));
3802
+ const pipePosition = (parent, argument, context, table) => Option.flatMap(argumentIndex(parent.arguments, argument), (index) => Option.orElse(onlyWhen(isPipeMethodCall(parent), {
3803
+ index,
3804
+ callee: "method"
3805
+ }), () => onlyWhen(isEffectPipeCall(parent, context, table), {
3806
+ index,
3807
+ callee: "effect"
3808
+ })));
3809
+ const formForPosition = (position) => Match.value(position.callee).pipe(Match.when((callee) => callee === "method", () => Option.some(DATA_LAST_PIPE_METHOD)), Match.orElse(() => onlyWhen(position.index >= 1, DATA_LAST_PIPE_ARG)));
3810
+ const argumentIndex = (args, node) => Option.flatMap(Option.fromNullishOr(args.findIndex((argument) => sameNode(argument, node))), (index) => onlyWhen(index >= 0, index));
3811
+ const parentCall = (context) => Option.flatMap(Option.fromNullishOr(context.parent), (parent) => Match.value(parent).pipe(Match.when(isCallExpression$1, (call) => Option.some(call)), Match.orElse(() => Option.none())));
3812
+ const isPipeMethodCall = (parent) => Match.value(parent.callee).pipe(Match.when(isMemberExpression, (member) => Option.exists(staticMemberName(member), (name) => name === "pipe")), Match.orElse(() => false));
3813
+ const isEffectPipeCall = (parent, context, table) => Match.value(parent.callee).pipe(Match.when(isIdentifier$1, (reference) => Bool.and(isVisible(reference.name, context), table.pipeBindings.has(reference.name))), Match.when(isMemberExpression, (member) => Option.exists(staticMemberName(member), (name) => Bool.and(name === "pipe", isPipeNamespace(member.object, context, table)))), Match.orElse(() => false));
3814
+ const isPipeNamespace = (expression, context, table) => Match.value(expression).pipe(Match.when(isIdentifier$1, (reference) => Option.match(namespaceOf(expression, context, table), {
3815
+ onNone: () => false,
3816
+ onSome: (binding) => Bool.and(isPipeNamespaceBinding(binding), isVisible(reference.name, context))
3817
+ })), Match.orElse(() => false));
3818
+ const isPipeNamespaceBinding = (binding) => Match.value(binding.kind).pipe(Match.when((kind) => kind === "root", () => true), Match.when((kind) => kind === "functionModule", () => true), Match.orElse(() => false));
3819
+ const moduleAccessFunction = (callee, context, table) => (module) => Arr.findFirst([
3820
+ ownerAccessFor(callee, module),
3821
+ namedModuleAccess(module, context, table),
3822
+ rootNamespaceAccess(module, context, table),
3823
+ moduleNamespaceAccess(module, context, table)
3824
+ ], Option.isSome).pipe(Option.flatten);
3825
+ const ownerAccessFor = (callee, module) => Option.flatMap(callee.owner, (owner) => onlyWhen(owner.module === module, owner.access));
3826
+ const namedModuleAccess = (module, context, table) => Option.map(Option.fromNullishOr([...table.moduleBindings].find(([local, bound]) => Bool.and(bound === module, isVisible(local, context)))), ([local]) => identifier(local));
3827
+ const rootNamespaceAccess = (module, context, table) => Option.map(Option.fromNullishOr([...table.namespaces].find(([local, binding]) => Bool.and(binding.kind === "root", isVisible(local, context)))), ([local]) => memberExpression(identifier(local), identifier(module), false));
3828
+ const moduleNamespaceAccess = (module, context, table) => Option.map(Option.fromNullishOr([...table.namespaces].find(([local, binding]) => Bool.and(Option.exists(moduleOf(binding), (bound) => bound === module), isVisible(local, context)))), ([local]) => identifier(local));
3829
+ const isMovableArgument = (node) => nodesOutsideFunctions(node).every((candidate) => YIELD_OR_AWAIT_KINDS[nodeType(candidate) ?? ""] !== true);
3830
+ const YIELD_OR_AWAIT_KINDS = {
3831
+ AwaitExpression: true,
3832
+ YieldExpression: true
3833
+ };
3834
+ const isDroppableArgument = (node) => Match.value(node).pipe(Match.when(isIdentifier$1, () => true), Match.when(isLiteral, () => true), Match.when(isThisExpression, () => true), Match.when(isFunctionLike, () => true), Match.when(isMemberExpression, (member) => isCallFreeMemberChain(member)), Match.orElse(() => false));
3835
+ const isCallFreeMemberChain = (member) => Match.value(member.computed).pipe(Match.when(true, () => false), Match.orElse(() => Match.value(member.object).pipe(Match.when(isIdentifier$1, () => true), Match.when(isThisExpression, () => true), Match.when(isMemberExpression, (inner) => isCallFreeMemberChain(inner)), Match.orElse(() => false))));
3836
+ const identifiersIn = (node) => allNodesUnder(node).flatMap((candidate) => Option.toArray(identifierText(candidate)));
3837
+ const freshIdentifier = (base, taken) => freeIdentifier(base, taken, 0);
3838
+ const freeIdentifier = (base, taken, suffix) => Match.value(taken.has(suffixedName(base, suffix))).pipe(Match.when(false, () => suffixedName(base, suffix)), Match.orElse(() => freeIdentifier(base, taken, suffix + 1)));
3839
+ const suffixedName = (base, suffix) => Match.value(suffix).pipe(Match.when(0, () => base), Match.orElse((count) => `${base}${count}`));
3840
+ const isShadowed = (name, context) => context.ancestors.some((ancestor) => scopeDeclares(ancestor, name));
3841
+ const isVisible = (name, context) => isShadowed(name, context) === false;
3842
+ const unshadowedName = (name, context) => onlyWhen(isVisible(name, context), name);
3843
+ const scopeDeclares = (ancestor, name) => Match.value(ancestor).pipe(Match.when(isProgram, (program) => statementsDeclare(program.body, name)), Match.when(isBlockStatement, (block) => statementsDeclare(block.body, name)), Match.when(isStaticBlock, (block) => statementsDeclare(block.body, name)), Match.when(isTSModuleBlock, (block) => statementsDeclare(block.body, name)), Match.when(isSwitchStatement, (statement) => statement.cases.some((caseClause) => statementsDeclare(caseClause.consequent, name))), Match.when(isFunctionLike, (fn) => functionDeclares(fn, name)), Match.when(isCatchClause, (clause) => patternDeclaresOr(clause.param, name)), Match.when(isForStatement, (statement) => variableDeclarationDeclaresOr(statement.init, name)), Match.when(isForInStatement, (statement) => variableDeclarationDeclaresOr(statement.left, name)), Match.when(isForOfStatement, (statement) => variableDeclarationDeclaresOr(statement.left, name)), Match.when(isClassLike, (cls) => declaresIdentifier(cls.id, name)), Match.orElse(() => false));
3844
+ const functionDeclares = (fn, name) => holdsAny([
3845
+ fn.params.some((param) => patternDeclaresOr(param, name)),
3846
+ declaresIdentifier(fn.id, name),
3847
+ blockBodyDeclaresHoistedVar(fn.body, name)
3848
+ ]);
3849
+ const blockBodyDeclaresHoistedVar = (body, name) => Option.match(Option.fromNullishOr(body), {
3850
+ onNone: () => false,
3851
+ onSome: (present) => Match.value(present).pipe(Match.when(isBlockStatement, (block) => hoistedVarsDeclare(block, name)), Match.orElse(() => false))
3852
+ });
3853
+ const hoistedVarsDeclare = (block, name) => nodesOutsideFunctions(block).some((candidate) => hoistedCandidateDeclares(candidate, name));
3854
+ const hoistedCandidateDeclares = (candidate, name) => Match.value(candidate).pipe(Match.when(isVariableDeclaration, (declaration) => Bool.and(declaration.kind === "var", statementBindingsDeclare(declaration, name))), Match.orElse(() => false));
3855
+ const statementsDeclare = (statements, name) => statements.some((statement) => statementDeclares(statement, name));
3856
+ const statementDeclares = (statement, name) => Match.value(statement).pipe(Match.when(isVariableDeclaration, (declaration) => statementBindingsDeclare(declaration, name)), Match.when(isFunctionDeclaration, (declaration) => declaresIdentifier(declaration.id, name)), Match.when(isClassLike, (declaration) => declaresIdentifier(declaration.id, name)), Match.when(isTSEnumDeclaration, (declaration) => declaration.id.name === name), Match.when(isTSModuleDeclaration, (declaration) => moduleDeclarationDeclares(declaration, name)), Match.when(isTSImportEqualsDeclaration, (declaration) => importEqualsDeclares(declaration, name)), Match.when(isExportNamedDeclaration, (declaration) => Option.exists(Option.fromNullishOr(declaration.declaration), (inner) => statementDeclares(inner, name))), Match.orElse(() => false));
3857
+ const statementBindingsDeclare = (declaration, name) => declaration.declarations.some((declarator) => patternDeclaresOr(declarator.id, name));
3858
+ const variableDeclarationDeclaresOr = (node, name) => Option.match(Option.fromNullishOr(node), {
3859
+ onNone: () => false,
3860
+ onSome: (present) => Match.value(present).pipe(Match.when(isVariableDeclaration, (declaration) => statementBindingsDeclare(declaration, name)), Match.orElse(() => false))
3861
+ });
3862
+ const moduleDeclarationDeclares = (declaration, name) => Bool.and(declaration.body !== null, Option.exists(identifierText(declaration.id), (text) => text === name));
3863
+ const importEqualsDeclares = (declaration, name) => Bool.and(declaration.importKind !== "type", declaration.id.name === name);
3864
+ const declaresIdentifier = (id, name) => Option.exists(identifierText(id), (text) => text === name);
3865
+ const patternDeclaresOr = (pattern, name) => Option.match(Option.fromNullishOr(pattern), {
3866
+ onNone: () => false,
3867
+ onSome: (present) => Match.value(present).pipe(Match.when(isBindingIdentifierPattern, (binding) => binding.name === name), Match.when(isObjectBindingPattern, (object) => object.properties.some((property) => propertyDeclares(property, name))), Match.when(isArrayBindingPattern, (array) => array.elements.some((element) => patternDeclaresOr(element, name))), Match.when(isAssignmentBindingPattern, (assignment) => patternDeclaresOr(assignment.left, name)), Match.when(isBindingRestElement, (rest) => patternDeclaresOr(rest.argument, name)), Match.orElse(() => false))
3868
+ });
3869
+ const propertyDeclares = (property, name) => Match.value(property).pipe(Match.when(isBindingProperty, (binding) => patternDeclaresOr(binding.value, name)), Match.when(isBindingRestElement, (rest) => patternDeclaresOr(rest.argument, name)), Match.orElse(() => false));
3870
+ const isBindingIdentifierPattern = (candidate) => nodeType(candidate) === "Identifier";
3871
+ const isObjectBindingPattern = (candidate) => nodeType(candidate) === "ObjectPattern";
3872
+ const isArrayBindingPattern = (candidate) => nodeType(candidate) === "ArrayPattern";
3873
+ const isAssignmentBindingPattern = (candidate) => nodeType(candidate) === "AssignmentPattern";
3874
+ const isBindingRestElement = (candidate) => nodeType(candidate) === "RestElement";
3875
+ const isBindingProperty = (candidate) => nodeType(candidate) === "Property";
3876
+ const identifierText = (node) => Option.match(Option.fromNullishOr(node), {
3877
+ onNone: () => Option.none(),
3878
+ onSome: (present) => Match.value(present).pipe(Match.when(isNamedIdentifier, (name) => Option.some(name.name)), Match.when(isStringLiteral$1, (literal) => literalValueText(literal.value)), Match.orElse(() => Option.none()))
3879
+ });
3880
+ const literalValueText = (value) => Match.value(value).pipe(Match.when(Predicate.isString, (text) => Option.some(text)), Match.orElse(() => Option.none()));
3881
+ const moduleExportName = (name) => Option.getOrElse(identifierText(name), () => "");
3882
+ const staticMemberName = (member) => Match.value(member.computed).pipe(Match.when(false, () => identifierText(member.property)), Match.orElse(() => Option.none()));
3883
+ const nodesOutsideFunctions = (root) => {
3884
+ const collected = [];
3885
+ traverse(root, { enter(path) {
3886
+ collected.push(path.node);
3887
+ pruneAtFunction(path);
3888
+ } });
3889
+ return collected;
3890
+ };
3891
+ const allNodesUnder = (root) => {
3892
+ const collected = [];
3893
+ traverse(root, { enter(path) {
3894
+ collected.push(path.node);
3895
+ } });
3896
+ return collected;
3897
+ };
3898
+ const pruneAtFunction = (path) => Option.match(onlyWhen(isFunctionLike(path.node), path), {
3899
+ onNone: () => void 0,
3900
+ onSome: (atFunction) => atFunction.skip()
3901
+ });
3902
+ const moduleCall = (module, property, args) => callExpression(memberExpression(module, identifier(property), false), args);
3903
+ const onlyWhen = (holds, value) => Match.value(holds).pipe(Match.when(true, () => Option.some(value)), Match.orElse(() => Option.none()));
3904
+ const holdsAny = (conditions) => conditions.some((condition) => condition);
3905
+ const sameNode = (first, second) => first === second;
3906
+ const FUNCTION_KINDS = {
3907
+ ArrowFunctionExpression: true,
3908
+ FunctionDeclaration: true,
3909
+ FunctionExpression: true
3910
+ };
3911
+ const CLASS_KINDS = {
3912
+ ClassDeclaration: true,
3913
+ ClassExpression: true
3914
+ };
3915
+ const isFunctionLike = (candidate) => FUNCTION_KINDS[nodeType(candidate) ?? ""] === true;
3916
+ const isClassLike = (candidate) => CLASS_KINDS[nodeType(candidate) ?? ""] === true;
3917
+ const isProgram = (candidate) => nodeType(candidate) === "Program";
3918
+ const isBlockStatement = (candidate) => nodeType(candidate) === "BlockStatement";
3919
+ const isStaticBlock = (candidate) => nodeType(candidate) === "StaticBlock";
3920
+ const isTSModuleBlock = (candidate) => nodeType(candidate) === "TSModuleBlock";
3921
+ const isSwitchStatement = (candidate) => nodeType(candidate) === "SwitchStatement";
3922
+ const isCatchClause = (candidate) => nodeType(candidate) === "CatchClause";
3923
+ const isForStatement = (candidate) => nodeType(candidate) === "ForStatement";
3924
+ const isForInStatement = (candidate) => nodeType(candidate) === "ForInStatement";
3925
+ const isForOfStatement = (candidate) => nodeType(candidate) === "ForOfStatement";
3926
+ const isVariableDeclaration = (candidate) => nodeType(candidate) === "VariableDeclaration";
3927
+ const isFunctionDeclaration = (candidate) => nodeType(candidate) === "FunctionDeclaration";
3928
+ const isTSEnumDeclaration = (candidate) => nodeType(candidate) === "TSEnumDeclaration";
3929
+ const isTSModuleDeclaration = (candidate) => nodeType(candidate) === "TSModuleDeclaration";
3930
+ const isTSImportEqualsDeclaration = (candidate) => nodeType(candidate) === "TSImportEqualsDeclaration";
3931
+ const isExportNamedDeclaration = (candidate) => nodeType(candidate) === "ExportNamedDeclaration";
3932
+ const isImportDeclaration$1 = (candidate) => nodeType(candidate) === "ImportDeclaration";
3933
+ const isNamedImportSpecifier = (candidate) => nodeType(candidate) === "ImportSpecifier";
3934
+ const isNamespaceImportSpecifier = (candidate) => nodeType(candidate) === "ImportNamespaceSpecifier";
3935
+ const isCallExpression$1 = (candidate) => nodeType(candidate) === "CallExpression";
3936
+ const isMemberExpression = (candidate) => nodeType(candidate) === "MemberExpression";
3937
+ const isIdentifier$1 = (candidate) => nodeType(candidate) === "Identifier";
3938
+ const isNamedIdentifier = (candidate) => nodeType(candidate) === "Identifier";
3939
+ const isThisExpression = (candidate) => nodeType(candidate) === "ThisExpression";
3940
+ const isLiteral = (candidate) => nodeType(candidate) === "Literal";
3941
+ const isStringLiteral$1 = (candidate) => nodeType(candidate) === "Literal";
3942
+ const isSpreadElement = (argument) => nodeType(argument) === "SpreadElement";
3943
+ const isSpreadFree = (argument) => nodeType(argument) !== "SpreadElement";
3944
+ //#endregion
3945
+ //#region src/AtomicUpdateSplit.ts
3946
+ const NO_MUTANTS$3 = [];
3947
+ const ATOMIC_UPDATE_SPLIT_MODULES = ["Ref", "SynchronizedRef"];
3948
+ const ATOMIC_UPDATE_SPLIT_OPERATIONS = [
3949
+ "modify",
3950
+ "modifySome",
3951
+ "update",
3952
+ "updateSome",
3953
+ "updateAndGet",
3954
+ "getAndUpdate"
3955
+ ];
3956
+ const atomicUpdateSplitMutator = (node, context) => Option.match(resolveEffectCall(node, context), {
3957
+ onNone: () => NO_MUTANTS$3,
3958
+ onSome: (call) => Option.toArray(atomicUpdateSplitReplacement(call))
3959
+ });
3960
+ const atomicUpdateSplitReplacement = (call) => Option.flatMap(splitOperation(call), (operation) => Option.some(replacementExpression$1(operation)));
3961
+ const splitOperation = (call) => Match.value(call).pipe(Match.when(isAtomicUpdateSplitOperation, (covered) => splitOperationOf(covered)), Match.orElse(() => Option.none()));
3962
+ const isAtomicUpdateSplitOperation = (call) => Bool.and(included(ATOMIC_UPDATE_SPLIT_MODULES, call.module), included(ATOMIC_UPDATE_SPLIT_OPERATIONS, call.exportName));
3963
+ const splitOperationOf = (call) => Option.flatMap(moduleBindings(call), (bindings) => Option.flatMap(splitArguments$1(call), (args) => Option.some({
3964
+ effectModule: bindings.effectModule,
3965
+ operationModule: bindings.operationModule,
3966
+ exportName: call.exportName,
3967
+ ref: args.ref,
3968
+ f: args.f,
3969
+ binders: freshBinders$1(takenNames$2(call, bindings, args.f))
3970
+ })));
3971
+ const splitArguments$1 = (call) => Match.value(call.form).pipe(Match.when((form) => form === "data-first", () => dataFirstArguments$1(call)), Match.when((form) => form === "piped-reference", () => Option.none()), Match.orElse(() => dataLastArguments(call)));
3972
+ const dataFirstArguments$1 = (call) => Option.flatMap(Arr.get(call.args, 0), (ref) => Option.flatMap(Arr.get(call.args, 1), (f) => onlyWhen(Bool.and(isMovableArgument(ref), isMovableArgument(f)), {
3973
+ ref: Option.some(cloneNode(ref)),
3974
+ f: cloneNode(f)
3975
+ })));
3976
+ const dataLastArguments = (call) => Option.flatMap(Arr.get(call.args, 0), (f) => onlyWhen(isMovableArgument(f), {
3977
+ ref: Option.none(),
3978
+ f: cloneNode(f)
3979
+ }));
3980
+ const moduleBindings = (call) => Option.flatMap(call.moduleAccess("Effect"), (effectModule) => Option.map(call.moduleAccess(call.module), (operationModule) => ({
3981
+ effectModule,
3982
+ operationModule
3983
+ })));
3984
+ const takenNames$2 = (call, bindings, f) => /* @__PURE__ */ new Set([
3985
+ ...call.args.flatMap((argument) => identifiersIn(argument)),
3986
+ ...identifiersIn(f),
3987
+ ...identifiersIn(bindings.effectModule),
3988
+ ...identifiersIn(bindings.operationModule)
3989
+ ]);
3990
+ const freshBinders$1 = (taken) => ({
3991
+ self: freshIdentifier("self", taken),
3992
+ s: freshIdentifier("s", taken),
3993
+ snap: freshIdentifier("snap", taken),
3994
+ b: freshIdentifier("b", taken),
3995
+ a: freshIdentifier("a", taken)
3996
+ });
3997
+ const replacementExpression$1 = (operation) => Option.match(operation.ref, {
3998
+ onNone: () => arrowFunctionExpression([identifier(operation.binders.self)], splitBody(operation)),
3999
+ onSome: (ref) => moduleCall(operation.effectModule, "flatMap", [moduleCall(operation.effectModule, "succeed", [ref]), arrowFunctionExpression([identifier(operation.binders.self)], splitBody(operation))])
4000
+ });
4001
+ const splitBody = (operation) => moduleCall(operation.effectModule, "flatMap", [moduleCall(operation.operationModule, "get", [identifier(operation.binders.self)]), arrowFunctionExpression([identifier(operation.binders.s)], moduleCall(operation.effectModule, "flatMap", [moduleCall(operation.operationModule, "make", [identifier(operation.binders.s)]), arrowFunctionExpression([identifier(operation.binders.snap)], moduleCall(operation.effectModule, "flatMap", [moduleCall(operation.operationModule, operation.exportName, [identifier(operation.binders.snap), operation.f]), arrowFunctionExpression([identifier(operation.binders.b)], moduleCall(operation.effectModule, "flatMap", [memberExpression(operation.effectModule, identifier("yieldNow"), false), arrowFunctionExpression([], moduleCall(operation.effectModule, "flatMap", [moduleCall(operation.operationModule, "get", [identifier(operation.binders.snap)]), arrowFunctionExpression([identifier(operation.binders.a)], moduleCall(operation.effectModule, "as", [moduleCall(operation.operationModule, "set", [identifier(operation.binders.self), identifier(operation.binders.a)]), identifier(operation.binders.b)]))]))]))]))]))]);
4002
+ const included = (values, value) => values.includes(value);
4003
+ //#endregion
4004
+ //#region src/FinalizerEscape.ts
4005
+ const NO_MUTANTS$2 = [];
4006
+ const COVERED_MODULE = "Effect";
4007
+ const TAG = "_tag";
4008
+ const SUCCESS_TAG = "Success";
4009
+ const INTERRUPT_TAG = "Interrupt";
4010
+ const finalizerEscapeMutator = (node, context) => Option.match(resolveEffectCall(node, context), {
4011
+ onNone: () => NO_MUTANTS$2,
4012
+ onSome: (call) => Option.toArray(replacementOf(call))
4013
+ });
4014
+ const replacementOf = (call) => Option.flatMap(onlyWhen(call.module === COVERED_MODULE, call), (covered) => Option.flatMap(covered.moduleAccess(COVERED_MODULE), (effect) => Option.flatMap(coveredForm(covered), (form) => Option.flatMap(splitArguments(covered, form), (split) => Option.flatMap(shapeOf(covered.exportName, split.rest), (shape) => Option.some(replacementExpression(call, effect, form, split, shape)))))));
4015
+ const replacementExpression = (call, effect, form, split, shape) => {
4016
+ const binders = freshBinders(takenNames$1(call, effect));
4017
+ const plan = {
4018
+ effect,
4019
+ self: selfExpression(split, binders),
4020
+ shape,
4021
+ binders
4022
+ };
4023
+ return placedBody(form, bodyExpression(plan), binders);
4024
+ };
4025
+ const coveredForm = (call) => Match.value(call.form).pipe(Match.when("data-first", () => Option.some("data-first")), Match.when("data-last-pipe-arg", () => Option.some("data-last")), Match.when("data-last-pipe-method", () => Option.some("data-last")), Match.when("data-last-immediate", () => Option.some("data-last")), Match.orElse(() => Option.none()));
4026
+ const splitArguments = (call, form) => Match.value(form).pipe(Match.when("data-first", () => dataFirstArguments(call)), Match.orElse(() => Option.some({
4027
+ self: Option.none(),
4028
+ rest: [...call.args]
4029
+ })));
4030
+ const dataFirstArguments = (call) => Option.map(Arr.get(call.args, 0), (self) => ({
4031
+ self: Option.some(cloneNode(self)),
4032
+ rest: call.args.slice(1)
4033
+ }));
4034
+ const selfExpression = (split, binders) => Option.getOrElse(split.self, () => identifier(binders.self));
4035
+ const shapeOf = (exportName, rest) => Match.value(exportName).pipe(Match.when("ensuring", () => finalizerShape(rest)), Match.when("onExit", () => exitShape(rest)), Match.when("onError", () => handlerShape(rest)), Match.when("onInterrupt", () => droppedHandlerShape(rest)), Match.when("acquireRelease", () => releaseShape(rest)), Match.when("acquireUseRelease", () => useReleaseShape(rest)), Match.orElse(() => Option.none()));
4036
+ const finalizerShape = (rest) => Option.flatMap(Arr.get(rest, 0), (fin) => onlyWhen(isMovableArgument(fin), {
4037
+ kind: "ensuring",
4038
+ fin
4039
+ }));
4040
+ const exitShape = (rest) => Option.map(Arr.get(rest, 0), (f) => ({
4041
+ kind: "onExit",
4042
+ f
4043
+ }));
4044
+ const handlerShape = (rest) => Option.map(Arr.get(rest, 0), (h) => ({
4045
+ kind: "onError",
4046
+ h
4047
+ }));
4048
+ const droppedHandlerShape = (rest) => Option.flatMap(Arr.get(rest, 0), (h) => onlyWhen(isDroppableArgument(h), {
4049
+ kind: "onInterrupt",
4050
+ h
4051
+ }));
4052
+ const releaseShape = (rest) => Option.flatMap(Arr.get(rest, 0), (rel) => Option.flatMap(droppableOptionAt(rest, 1), (optionsDroppable) => onlyWhen(Bool.and(isMovableArgument(rel), optionsDroppable), {
4053
+ kind: "acquireRelease",
4054
+ rel
4055
+ })));
4056
+ const useReleaseShape = (rest) => Option.flatMap(Arr.get(rest, 1), (rel) => Option.flatMap(Arr.get(rest, 0), (use) => Option.flatMap(releaseArityOf(rel), (releaseArity) => onlyWhen(isMovableArgument(rel), {
4057
+ kind: "acquireUseRelease",
4058
+ use,
4059
+ rel,
4060
+ releaseArity
4061
+ }))));
4062
+ const droppableOptionAt = (rest, index) => Option.match(Arr.get(rest, index), {
4063
+ onNone: () => Option.some(true),
4064
+ onSome: (argument) => onlyWhen(isDroppableArgument(argument), true)
4065
+ });
4066
+ const bodyExpression = (plan) => Match.value(plan.shape).pipe(Match.when({ kind: "ensuring" }, (shape) => moduleCall(plan.effect, "onExitIf", [
4067
+ plan.self,
4068
+ notInterruptedPredicate(plan.binders),
4069
+ arrowFunctionExpression([], shape.fin)
4070
+ ])), Match.when({ kind: "onExit" }, (shape) => moduleCall(plan.effect, "onExitIf", [
4071
+ plan.self,
4072
+ notInterruptedPredicate(plan.binders),
4073
+ shape.f
4074
+ ])), Match.when({ kind: "onError" }, (shape) => moduleCall(plan.effect, "onErrorIf", [
4075
+ plan.self,
4076
+ noInterruptPredicate(plan.binders),
4077
+ shape.h
4078
+ ])), Match.when({ kind: "onInterrupt" }, () => plan.self), Match.when({ kind: "acquireRelease" }, (shape) => releaseBody(plan, shape)), Match.when({ kind: "acquireUseRelease" }, (shape) => useReleaseBody(plan, shape)), Match.exhaustive);
4079
+ const releaseBody = (plan, shape) => moduleCall(plan.effect, "flatMap", [moduleCall(plan.effect, "interruptible", [plan.self]), arrowFunctionExpression([identifier(plan.binders.a)], moduleCall(plan.effect, "acquireRelease", [moduleCall(plan.effect, "succeed", [identifier(plan.binders.a)]), shape.rel]))]);
4080
+ const useReleaseBody = (plan, shape) => moduleCall(plan.effect, "acquireUseRelease", [
4081
+ plan.self,
4082
+ shape.use,
4083
+ arrowFunctionExpression([identifier(plan.binders.a), identifier(plan.binders.exit)], conditionalExpression(notInterruptedTest(plan.binders), appliedRelease(shape.rel, plan.binders, shape.releaseArity), memberExpression(plan.effect, identifier("void"), false)))
4084
+ ]);
4085
+ const appliedRelease = (rel, binders, releaseArity) => callExpression(rel, Arr.take([identifier(binders.a), identifier(binders.exit)], releaseArity));
4086
+ const releaseArityOf = (rel) => Match.value(rel).pipe(Match.when(isInlineFunction, plainParameterArity), Match.orElse(() => Option.none()));
4087
+ const isInlineFunction = (candidate) => nodeType(candidate) === "ArrowFunctionExpression" || nodeType(candidate) === "FunctionExpression";
4088
+ const plainParameterArity = (fn) => onlyWhen(Bool.and(fn.params.every(isPlainParameter), fn.params.length <= 2), fn.params.length);
4089
+ const isPlainParameter = (param) => nodeType(param) === "Identifier";
4090
+ const placedBody = (form, body, binders) => Match.value(form).pipe(Match.when("data-first", () => body), Match.orElse(() => arrowFunctionExpression([identifier(binders.self)], body)));
4091
+ const notInterruptedPredicate = (binders) => arrowFunctionExpression([identifier(binders.exit)], notInterruptedTest(binders));
4092
+ const notInterruptedTest = (binders) => logicalExpression("||", tagCheck(binders.exit, SUCCESS_TAG), unaryExpression("!", interruptionSome(failureReasons(binders.exit), binders.reason)));
4093
+ const noInterruptPredicate = (binders) => arrowFunctionExpression([identifier(binders.cause)], unaryExpression("!", interruptionSome(causeReasons(binders.cause), binders.reason)));
4094
+ const tagCheck = (name, tag) => binaryExpression("===", memberExpression(identifier(name), identifier(TAG), false), stringLiteral(tag));
4095
+ const failureReasons = (exitName) => memberExpression(memberExpression(identifier(exitName), identifier("cause"), false), identifier("reasons"), false);
4096
+ const causeReasons = (causeName) => memberExpression(identifier(causeName), identifier("reasons"), false);
4097
+ const interruptionSome = (reasons, reasonName) => callExpression(memberExpression(reasons, identifier("some"), false), [arrowFunctionExpression([identifier(reasonName)], tagCheck(reasonName, INTERRUPT_TAG))]);
4098
+ const takenNames$1 = (call, effect) => /* @__PURE__ */ new Set([...call.args.flatMap((argument) => identifiersIn(argument)), ...identifiersIn(effect)]);
4099
+ const freshBinders = (taken) => ({
4100
+ self: freshIdentifier("self", taken),
4101
+ exit: freshIdentifier("exit", taken),
4102
+ cause: freshIdentifier("cause", taken),
4103
+ reason: freshIdentifier("reason", taken),
4104
+ a: freshIdentifier("a", taken)
4105
+ });
4106
+ //#endregion
4107
+ //#region src/SynchronizationRemoval.ts
4108
+ /**
4109
+ * SynchronizationRemoval — the R7 mutants: a removed lock and a removed mask.
4110
+ *
4111
+ * Four call shapes, each producing the wrapped effect alone:
4112
+ *
4113
+ * - `Semaphore.withPermits(sem, n, e)` and `Semaphore.withPermit(sem, e)` become
4114
+ * the guarded effect. The guard arguments are dropped, so they must be
4115
+ * droppable (KTD5); the effect itself is cloned so the mutant's tree shares
4116
+ * nothing with the original's.
4117
+ * - Their data-last forms become `(self) => self`, which the resolver only
4118
+ * reports where that function is contextually typed (R4, KTD9) — a piped
4119
+ * argument, a piped method argument, or the callee of an immediate call.
4120
+ * - `Effect.uninterruptible(e)` becomes `e`, and a bare `Effect.uninterruptible`
4121
+ * used as a pipe argument becomes `(self) => self` (KTD9).
4122
+ * - `Effect.uninterruptibleMask(f)` becomes `E.suspend(() => f(E.interruptible))`
4123
+ * (KTD7): `f` is called at run time, as the original calls it, and the restore
4124
+ * it receives is the identity wherever the surrounding region is interruptible.
4125
+ * This needs the Effect module object in scope (R2, KTD3).
4126
+ *
4127
+ * Resolution lives in `EffectCall.ts`; this module never inspects imports. It is
4128
+ * a pure function from a resolved call to the one mutant it implies, and yields
4129
+ * no mutant whenever the replacement would evaluate an argument twice or lean on
4130
+ * a moved argument that can yield.
4131
+ */
4132
+ const NO_MUTANTS$1 = [];
4133
+ const synchronizationRemovalMutator = (node, context) => Option.match(resolveEffectCall(node, context), {
4134
+ onNone: () => NO_MUTANTS$1,
4135
+ onSome: (call) => Option.toArray(synchronizationRemovalReplacement(call))
4136
+ });
4137
+ const synchronizationRemovalReplacement = (call) => Match.value(call).pipe(Match.when(isWithPermits, withPermitsReplacement), Match.when(isWithPermit, withPermitReplacement), Match.when(isUninterruptible, uninterruptibleReplacement), Match.when(isUninterruptibleMask, uninterruptibleMaskReplacement), Match.orElse(() => Option.none()));
4138
+ const isWithPermits = (call) => Bool.and(call.module === "Semaphore", call.exportName === "withPermits");
4139
+ const isWithPermit = (call) => Bool.and(call.module === "Semaphore", call.exportName === "withPermit");
4140
+ const isUninterruptible = (call) => Bool.and(call.module === "Effect", call.exportName === "uninterruptible");
4141
+ const isUninterruptibleMask = (call) => Bool.and(call.module === "Effect", call.exportName === "uninterruptibleMask");
4142
+ const isDataFirst = (form) => form === "data-first";
4143
+ const isPipedReference = (form) => form === "piped-reference";
4144
+ const withPermitsReplacement = (call) => Match.value(call.form).pipe(Match.when(isDataFirst, () => whenDroppable(call, [0, 1], (guarded) => argumentReplacement(guarded, 2))), Match.orElse(() => whenDroppable(call, [0, 1], identityFunctionOf)));
4145
+ const withPermitReplacement = (call) => Match.value(call.form).pipe(Match.when(isDataFirst, () => whenDroppable(call, [0], (guarded) => argumentReplacement(guarded, 1))), Match.orElse(() => whenDroppable(call, [0], identityFunctionOf)));
4146
+ const uninterruptibleReplacement = (call) => Match.value(call.form).pipe(Match.when(isPipedReference, () => Option.some(identityFunction(call))), Match.orElse(() => argumentReplacement(call, 0)));
4147
+ const uninterruptibleMaskReplacement = (call) => whenMovable(call, [0], (masked) => Option.flatMap(masked.moduleAccess("Effect"), (effectModule) => Option.map(Arr.get(masked.args, 0), (f) => maskedReplacement(effectModule, f))));
4148
+ const maskedReplacement = (effectModule, f) => moduleCall(effectModule, "suspend", [arrowFunctionExpression([], callExpression(cloneNode(f), [memberExpression(effectModule, identifier("interruptible"), false)]))]);
4149
+ const argumentReplacement = (call, index) => Option.map(Arr.get(call.args, index), (argument) => cloneNode(argument));
4150
+ const identityFunctionOf = (call) => Option.some(identityFunction(call));
4151
+ const identityFunction = (call) => {
4152
+ const name = freshIdentifier("self", takenNames(call));
4153
+ return arrowFunctionExpression([identifier(name)], identifier(name));
4154
+ };
4155
+ const takenNames = (call) => new Set(call.args.flatMap((argument) => identifiersIn(argument)));
4156
+ const whenDroppable = (call, indices, build) => Option.flatMap(onlyWhen(argumentsDroppable(call, indices), call), build);
4157
+ const whenMovable = (call, indices, build) => Option.flatMap(onlyWhen(argumentsMovable(call, indices), call), build);
4158
+ const argumentsDroppable = (call, indices) => indices.every((index) => argumentSatisfies(call, index, isDroppableArgument));
4159
+ const argumentsMovable = (call, indices) => indices.every((index) => argumentSatisfies(call, index, isMovableArgument));
4160
+ const argumentSatisfies = (call, index, predicate) => Option.exists(Arr.get(call.args, index), predicate);
4161
+ //#endregion
3559
4162
  //#region src/Mutator.ts
3560
4163
  /**
3561
4164
  * Mutator — every mutation operator and its registry.
@@ -4391,8 +4994,11 @@ function isUpdateExpression(node) {
4391
4994
  *
4392
4995
  * Naming each mutator here costs one line when a mutator is added and makes
4393
4996
  * that line a compile-checked import instead of a runtime effect.
4997
+ *
4998
+ * `optInMutators` below follows the same hand-written rule, for the same
4999
+ * reason: a mutator exists for a run only when a human named it here.
4394
5000
  */
4395
- const allMutators = Object.freeze({
5001
+ const defaultMutators = Object.freeze({
4396
5002
  ArithmeticOperator: arithmeticOperatorMutator,
4397
5003
  ArrayDeclaration: arrayDeclarationMutator,
4398
5004
  ArrowFunction: arrowFunctionMutator,
@@ -4410,6 +5016,28 @@ const allMutators = Object.freeze({
4410
5016
  UnaryOperator: unaryOperatorMutator,
4411
5017
  UpdateOperator: updateOperatorMutator
4412
5018
  });
5019
+ const optInMutators = Object.freeze({
5020
+ AtomicUpdateSplit: atomicUpdateSplitMutator,
5021
+ SynchronizationRemoval: synchronizationRemovalMutator,
5022
+ FinalizerEscape: finalizerEscapeMutator
5023
+ });
5024
+ /**
5025
+ * The entries a run applies, and the names its directives may reference.
5026
+ *
5027
+ * Naming is additive, never a whitelist: `optInMutations` adds entries on top
5028
+ * of the defaults and removes none. Selection walks the registry's declared
5029
+ * order, not the order the run listed its names in, so a mutant's identity
5030
+ * never depends on how a config happened to spell the list; a name listed
5031
+ * twice selects its entry once. An unknown name selects nothing here —
5032
+ * `instrument` refuses it before any file is parsed, because a typo that
5033
+ * silently enables nothing removes mutants and raises the score.
5034
+ *
5035
+ * Pure: a registry and a run's names in, entries and names out.
5036
+ */
5037
+ const selectMutators = (registry, optInMutations) => ({
5038
+ active: [...Object.entries(registry.defaults), ...Object.entries(registry.optIn).filter(([name]) => optInMutations.includes(name))],
5039
+ known: [...Object.keys(registry.defaults), ...Object.keys(registry.optIn)]
5040
+ });
4413
5041
  S.TaggedError("@systemfsoftware/stryker-js-instrumenter/Transformer.schema/PlacementFailed")("PlacementFailed", {
4414
5042
  fileName: S.String,
4415
5043
  placerName: S.String,
@@ -4425,6 +5053,10 @@ S.TaggedError("@systemfsoftware/stryker-js-instrumenter/Transformer.schema/Trans
4425
5053
  const STRYKER_NAMESPACE_HELPER = "stryNS_9fa48";
4426
5054
  const COVER_MUTANT_HELPER = "stryCov_9fa48";
4427
5055
  const IS_MUTANT_ACTIVE_HELPER = "stryMutAct_9fa48";
5056
+ const DEFAULT_MUTATOR_REGISTRY = {
5057
+ defaults: defaultMutators,
5058
+ optIn: optInMutators
5059
+ };
4428
5060
  function createMutantCollector() {
4429
5061
  return [];
4430
5062
  }
@@ -4460,11 +5092,11 @@ function directiveApplies(directive, lowerMutatorName, line) {
4460
5092
  const NO_COMMENTS = [];
4461
5093
  const PARSE_FAILURE = "Stryker directive without directive type or mutators";
4462
5094
  const MISSING_LOCATION = "Comment without location";
4463
- function processStrykerDirectives(rule, node, allMutatorNames, originFileName) {
5095
+ function processStrykerDirectives(rule, node, knownMutatorNames, originFileName, nodeLine) {
4464
5096
  const directives = attachedComments(node).map(parseStrykerDirective).flatMap(Option.toArray);
4465
- const warnings = directives.flatMap((directive) => mutatorWarnings(directive, allMutatorNames, originFileName));
5097
+ const warnings = directives.flatMap((directive) => mutatorWarnings(directive, knownMutatorNames, originFileName));
4466
5098
  return {
4467
- rule: directives.reduce(applyStrykerDirective, rule),
5099
+ rule: directives.map((directive) => nextLineOn(directive, nodeLine)).reduce(applyStrykerDirective, rule),
4468
5100
  warnings
4469
5101
  };
4470
5102
  }
@@ -4493,36 +5125,39 @@ function strykerDirective(match, loc) {
4493
5125
  function matchGroup(match, group) {
4494
5126
  return Option.getOrThrowWith(Option.fromNullishOr(match[group]), () => /* @__PURE__ */ new Error(PARSE_FAILURE));
4495
5127
  }
4496
- function applyStrykerDirective(rule, directive) {
4497
- return Match.value(directive.type).pipe(Match.when("disable", () => ignoreRuleFor(rule, directive)), Match.when("restore", () => restoreRuleFor(rule, directive)), Match.orElse(() => rule));
5128
+ const nextLineOn = (directive, nodeLine) => ({
5129
+ directive,
5130
+ nodeLine
5131
+ });
5132
+ function applyStrykerDirective(rule, directed) {
5133
+ return Match.value(directed.directive.type).pipe(Match.when("disable", () => ignoreRuleFor(rule, directed)), Match.when("restore", () => restoreRuleFor(rule, directed)), Match.orElse(() => rule));
4498
5134
  }
4499
- function ignoreRuleFor(rule, directive) {
5135
+ function ignoreRuleFor(rule, directed) {
4500
5136
  return {
4501
5137
  kind: "Ignore",
4502
- mutatorNames: directive.mutatorNames.map((mutatorName) => mutatorName.toLowerCase()),
4503
- line: directiveLine(directive),
4504
- ignoreReason: directive.reason,
5138
+ mutatorNames: directed.directive.mutatorNames.map((mutatorName) => mutatorName.toLowerCase()),
5139
+ line: directiveLine(directed),
5140
+ ignoreReason: directed.directive.reason,
4505
5141
  previous: rule
4506
5142
  };
4507
5143
  }
4508
- function restoreRuleFor(rule, directive) {
5144
+ function restoreRuleFor(rule, directed) {
4509
5145
  return {
4510
5146
  kind: "Restore",
4511
- mutatorNames: directive.mutatorNames.map((mutatorName) => mutatorName.toLowerCase()),
4512
- line: directiveLine(directive),
5147
+ mutatorNames: directed.directive.mutatorNames.map((mutatorName) => mutatorName.toLowerCase()),
5148
+ line: directiveLine(directed),
4513
5149
  previous: rule
4514
5150
  };
4515
5151
  }
4516
- /** `next-line` directives carry the line they were written on; a block directive carries none. */
4517
- function directiveLine(directive) {
4518
- return Match.value(directive.scope).pipe(Match.when("next-line", () => commentLocation(directive.loc).start.line), Match.orElse(() => void 0));
5152
+ function directiveLine(directed) {
5153
+ return Match.value(directed.directive.scope).pipe(Match.when("next-line", () => directed.nodeLine), Match.orElse(() => void 0));
4519
5154
  }
4520
5155
  function commentLocation(loc) {
4521
5156
  return Option.getOrThrowWith(Option.fromNullishOr(loc), () => /* @__PURE__ */ new Error(MISSING_LOCATION));
4522
5157
  }
4523
- /** Warnings for the directive's mutator names that the configured mutators do not know. */
4524
- function mutatorWarnings(directive, allMutatorNames, originFileName) {
4525
- return directive.mutatorNames.filter((mutatorName) => mutatorName !== WILDCARD).filter((mutatorName) => !allMutatorNames.includes(mutatorName.toLowerCase())).map((mutatorName) => mutatorWarning(directive, mutatorName, originFileName));
5158
+ /** Warnings for the directive's mutator names that no registered mutator answers to, selected or not. */
5159
+ function mutatorWarnings(directive, knownMutatorNames, originFileName) {
5160
+ return directive.mutatorNames.filter((mutatorName) => mutatorName !== WILDCARD).filter((mutatorName) => !knownMutatorNames.includes(mutatorName.toLowerCase())).map((mutatorName) => mutatorWarning(directive, mutatorName, originFileName));
4526
5161
  }
4527
5162
  function mutatorWarning(directive, mutatorName, originFileName) {
4528
5163
  const loc = commentLocation(directive.loc);
@@ -5008,19 +5643,21 @@ const emptyAppliedMutants = () => /* @__PURE__ */ new Map();
5008
5643
  function isMutateRangeList(value) {
5009
5644
  return Array.isArray(value);
5010
5645
  }
5011
- const transformScript = ({ root, originFileName, rawContent, offset, comments }, mutantCollector, { options, mutateDescription, basePath }, mutators, mutantPlacers) => {
5646
+ const transformScript = ({ root, originFileName, rawContent, offset, comments }, mutantCollector, { options, mutateDescription, basePath }, registry, mutantPlacers) => {
5012
5647
  const placementMap = /* @__PURE__ */ new Map();
5013
5648
  return Effect.gen(function* () {
5014
5649
  const lineTable = buildLineTable(rawContent);
5015
5650
  attachComments(root, comments, lineTable);
5016
5651
  let directiveRule = rootRule;
5017
- const mutatorEntries = Object.entries(Option.getOrElse(Option.fromNullishOr(mutators), () => allMutators));
5652
+ const mutatorRegistry = Option.getOrElse(Option.fromNullishOr(registry), () => DEFAULT_MUTATOR_REGISTRY);
5653
+ const selection = selectMutators(mutatorRegistry, options.optInMutations);
5654
+ const mutatorEntries = selection.active;
5018
5655
  const placers = Option.getOrElse(Option.fromNullishOr(mutantPlacers), () => allMutantPlacers);
5019
- const allMutatorNames = mutatorEntries.map(([name]) => name.toLowerCase());
5656
+ const knownMutatorNames = selection.known.map((name) => name.toLowerCase());
5020
5657
  const warnings = [];
5021
5658
  traverse(root, {
5022
5659
  enter(path) {
5023
- const result = processStrykerDirectives(directiveRule, path.node, allMutatorNames, originFileName);
5660
+ const result = processStrykerDirectives(directiveRule, path.node, knownMutatorNames, originFileName, nodeStartLine(path.node));
5024
5661
  directiveRule = result.rule;
5025
5662
  warnings.push(...result.warnings);
5026
5663
  visitNode(path);
@@ -5030,6 +5667,12 @@ const transformScript = ({ root, originFileName, rawContent, offset, comments },
5030
5667
  if (hasAppliedMutants(placement)) applyPlacement(path, placement);
5031
5668
  }
5032
5669
  });
5670
+ function nodeStartLine(node) {
5671
+ return Option.match(Option.fromNullishOr(spanOf(node)), {
5672
+ onNone: () => 0,
5673
+ onSome: (span) => positionFromLineTable(span.start, lineTable).line
5674
+ });
5675
+ }
5033
5676
  yield* placeHeaderIfNeeded(mutantCollector, originFileName, options, root);
5034
5677
  return warnings;
5035
5678
  function visitNode(path) {
@@ -5303,9 +5946,26 @@ function toOneBasedLineNumber(range) {
5303
5946
  function isIgnorer(value) {
5304
5947
  return Predicate.isObject(value) && typeof value["shouldIgnore"] === "function";
5305
5948
  }
5949
+ const EMPTY_MUTATION_NAMES = [];
5950
+ const KNOWN_OPT_IN_MUTATIONS = Object.keys(optInMutators);
5951
+ const requestedOptInMutations = (options) => Option.getOrElse(Option.fromNullishOr(options.optInMutations), () => EMPTY_MUTATION_NAMES);
5952
+ const unknownOptInMutations = (requested) => requested.filter((name) => !KNOWN_OPT_IN_MUTATIONS.includes(name));
5953
+ const listNames = (names) => Option.match(Option.fromNullishOr(names.at(0)), {
5954
+ onNone: () => "none",
5955
+ onSome: () => names.map((name) => `'${name}'`).join(", ")
5956
+ });
5957
+ const unknownOptInMutationsError = (requested) => Option.map(Option.fromNullishOr(unknownOptInMutations(requested).at(0)), () => InstrumentError.make({
5958
+ message: `Unknown opt-in mutations: ${listNames(unknownOptInMutations(requested))}. Known opt-in mutations: ${listNames(KNOWN_OPT_IN_MUTATIONS)}.`,
5959
+ cause: void 0
5960
+ }));
5961
+ const refuseUnknownOptInMutations = (requested) => Option.match(unknownOptInMutationsError(requested), {
5962
+ onNone: () => Effect.void,
5963
+ onSome: (error) => Effect.fail(error)
5964
+ });
5306
5965
  function toTransformerOptions(options) {
5307
5966
  const base = {
5308
5967
  excludedMutations: [...options.excludedMutations],
5968
+ optInMutations: [...requestedOptInMutations(options)],
5309
5969
  ignorers: options.ignorers.filter(isIgnorer)
5310
5970
  };
5311
5971
  if (options.noHeader !== void 0) return {
@@ -5327,6 +5987,7 @@ function printedFile(file, ast) {
5327
5987
  }];
5328
5988
  }
5329
5989
  const instrument = (files, options, basePath) => Effect.gen(function* () {
5990
+ yield* refuseUnknownOptInMutations(requestedOptInMutations(options));
5330
5991
  const schemaFiles = files.map((file) => ({
5331
5992
  name: file.name,
5332
5993
  content: file.content,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemfsoftware/stryker-js-instrumenter",
3
- "version": "8.0.2",
3
+ "version": "8.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/systemfsoftware/stryker-js-effect.git",
@@ -31,6 +31,7 @@
31
31
  "oxc-walker": "^1"
32
32
  },
33
33
  "devDependencies": {
34
+ "@effect/platform-node": "4.0.0-rc.116",
34
35
  "@effect/tsgo": "^0.45.0",
35
36
  "@microsoft/api-extractor": "^7",
36
37
  "@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
@@ -44,8 +45,8 @@
44
45
  "tsdown": "^0.23.0",
45
46
  "typescript": "^7",
46
47
  "vitest": "^5",
47
- "@systemfsoftware/stryker-ignorer-interface": "^0.1.1",
48
48
  "@systemfsoftware/tsdown-config": "^0.1.0",
49
+ "@systemfsoftware/stryker-ignorer-interface": "^0.1.1",
49
50
  "@systemfsoftware/vitest-config": "^0.1.0"
50
51
  },
51
52
  "engines": {