@fairfox/polly 0.76.0 → 0.77.1

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.
@@ -0,0 +1,32 @@
1
+ import type { Ignorer, NodePath } from "@stryker-mutator/api/ignore";
2
+ import { PluginKind } from "@stryker-mutator/api/plugin";
3
+ /**
4
+ * The polly verify primitives whose argument expressions are runtime no-ops.
5
+ * A mutation anywhere inside a call to one of these cannot be killed by a test,
6
+ * so its mutants are excluded from scoring.
7
+ */
8
+ export declare const VERIFY_PRIMITIVES: ReadonlySet<string>;
9
+ /** The Stryker plugin name consumers reference in `ignorers`. */
10
+ export declare const POLLY_VERIFY_IGNORER_NAME = "polly-verify";
11
+ /**
12
+ * A Stryker `Ignore` plugin. Stryker calls `shouldIgnore` on entering each AST
13
+ * node; returning a message marks that node — and every descendant, until the
14
+ * node is left — as ignored. So matching the verify `CallExpression` itself
15
+ * covers its condition and message arguments in one shot.
16
+ */
17
+ export declare class PollyVerifyIgnorer implements Ignorer {
18
+ private readonly primitives;
19
+ constructor(primitives?: ReadonlySet<string>);
20
+ shouldIgnore(path: NodePath): string | undefined;
21
+ }
22
+ /** The plugin array Stryker reads when this module is listed in `plugins`. */
23
+ export declare const strykerPlugins: import("@stryker-mutator/api/plugin").FactoryPlugin<PluginKind.Ignore, ["options"]>[];
24
+ /**
25
+ * A partial Stryker config that wires up the ignorer. Spread it into a
26
+ * `stryker.conf.mjs` default export, or replicate its two keys in JSON.
27
+ */
28
+ export declare const pollyStrykerPreset: {
29
+ readonly plugins: readonly ["@fairfox/polly/stryker"];
30
+ readonly ignorers: readonly ["polly-verify"];
31
+ };
32
+ export default pollyStrykerPreset;
@@ -0,0 +1,95 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
+
19
+ // tools/verify/src/stryker/index.ts
20
+ import {
21
+ commonTokens,
22
+ declareFactoryPlugin,
23
+ PluginKind,
24
+ tokens
25
+ } from "@stryker-mutator/api/plugin";
26
+ var VERIFY_PRIMITIVES = new Set([
27
+ "requires",
28
+ "ensures",
29
+ "invariant",
30
+ "stateConstraint",
31
+ "forAllPeers",
32
+ "somePeer"
33
+ ]);
34
+ var POLLY_VERIFY_IGNORER_NAME = "polly-verify";
35
+ function calleeName(callee) {
36
+ if (!callee)
37
+ return;
38
+ if (callee.type === "Identifier") {
39
+ return callee.name;
40
+ }
41
+ if (callee.type === "MemberExpression") {
42
+ const member = callee;
43
+ if (!member.computed && member.property.type === "Identifier") {
44
+ return member.property.name;
45
+ }
46
+ }
47
+ return;
48
+ }
49
+
50
+ class PollyVerifyIgnorer {
51
+ primitives;
52
+ constructor(primitives = VERIFY_PRIMITIVES) {
53
+ this.primitives = primitives;
54
+ }
55
+ shouldIgnore(path) {
56
+ const callPath = path;
57
+ if (typeof callPath.isCallExpression !== "function" || !callPath.isCallExpression()) {
58
+ return;
59
+ }
60
+ const name = calleeName(callPath.node.callee);
61
+ if (name && this.primitives.has(name)) {
62
+ return `Inside polly's ${name}(...) — a runtime no-op (compiled away in production, ` + `translated to a TLA+ assertion in verification). No test can observe or kill ` + `mutations here, so they are excluded from the score (polly#143).`;
63
+ }
64
+ return;
65
+ }
66
+ }
67
+ function isEnabled(options) {
68
+ const polly = options.polly;
69
+ return polly?.excludeVerifyCallsites !== false;
70
+ }
71
+ var NOOP_IGNORER = { shouldIgnore: () => {
72
+ return;
73
+ } };
74
+ function pollyVerifyIgnorerFactory(options) {
75
+ return isEnabled(options) ? new PollyVerifyIgnorer : NOOP_IGNORER;
76
+ }
77
+ pollyVerifyIgnorerFactory.inject = tokens(commonTokens.options);
78
+ var strykerPlugins = [
79
+ declareFactoryPlugin(PluginKind.Ignore, POLLY_VERIFY_IGNORER_NAME, pollyVerifyIgnorerFactory)
80
+ ];
81
+ var pollyStrykerPreset = {
82
+ plugins: ["@fairfox/polly/stryker"],
83
+ ignorers: [POLLY_VERIFY_IGNORER_NAME]
84
+ };
85
+ var stryker_default = pollyStrykerPreset;
86
+ export {
87
+ strykerPlugins,
88
+ pollyStrykerPreset,
89
+ stryker_default as default,
90
+ VERIFY_PRIMITIVES,
91
+ PollyVerifyIgnorer,
92
+ POLLY_VERIFY_IGNORER_NAME
93
+ };
94
+
95
+ //# debugId=D1E2AADE2B7A729B64756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../tools/verify/src/stryker/index.ts"],
4
+ "sourcesContent": [
5
+ "// Stryker mutation-testing ignorer for polly's verify primitives (polly#143).\n//\n// `requires`, `ensures`, `invariant`, `stateConstraint`, `forAllPeers`, and\n// `somePeer` are runtime no-ops: in production they compile away, in\n// verification they translate to TLA+ assertions. Nothing observes their\n// condition or message argument at test runtime, so EVERY mutation inside one\n// of these callsites is guaranteed to survive — a string-literal flip in an\n// `ensures(...)` message, an `===` → equality mutation in a `requires(...)`\n// condition. On a downstream project mutating six state-machine specs this\n// dragged the mutation score down to 21%, all of it noise rather than real\n// test-coverage gaps.\n//\n// Polly is the right place to ship this knowledge: it knows which of its\n// primitives are no-ops. This module is a Stryker `Ignore` plugin that marks\n// every mutant inside a verify callsite as ignored, plus a small config preset\n// consumers can spread into their `stryker.conf.*`.\n//\n// Usage (stryker.conf.json):\n//\n// {\n// \"plugins\": [\"@fairfox/polly/stryker\"],\n// \"ignorers\": [\"polly-verify\"]\n// }\n//\n// Or, in stryker.conf.mjs:\n//\n// import pollyStrykerPreset from \"@fairfox/polly/stryker\";\n// export default { ...pollyStrykerPreset, mutate: [\"src/**/*.ts\"] };\n//\n// Set `\"polly\": { \"excludeVerifyCallsites\": false }` to keep the plugin listed\n// but disable the ignoring (e.g. in a shared base config).\n\nimport type { StrykerOptions } from \"@stryker-mutator/api/core\";\nimport type { Ignorer, NodePath } from \"@stryker-mutator/api/ignore\";\nimport {\n commonTokens,\n declareFactoryPlugin,\n PluginKind,\n tokens,\n} from \"@stryker-mutator/api/plugin\";\n\n/**\n * The polly verify primitives whose argument expressions are runtime no-ops.\n * A mutation anywhere inside a call to one of these cannot be killed by a test,\n * so its mutants are excluded from scoring.\n */\nexport const VERIFY_PRIMITIVES: ReadonlySet<string> = new Set([\n \"requires\",\n \"ensures\",\n \"invariant\",\n \"stateConstraint\",\n \"forAllPeers\",\n \"somePeer\",\n]);\n\n/** The Stryker plugin name consumers reference in `ignorers`. */\nexport const POLLY_VERIFY_IGNORER_NAME = \"polly-verify\";\n\n// The Stryker API types `NodePath` as an empty interface; at runtime it is a\n// Babel NodePath. We narrow only the surface we touch — `isCallExpression()`\n// and `node.callee` — without pulling in @babel/types as a dependency.\ninterface BabelIdentifier {\n type: \"Identifier\";\n name: string;\n}\ninterface BabelMemberExpression {\n type: \"MemberExpression\";\n computed: boolean;\n property: { type: string; name?: string };\n}\ntype BabelCallee = BabelIdentifier | BabelMemberExpression | { type: string };\ninterface CallExpressionPath extends NodePath {\n isCallExpression(): boolean;\n node: { callee?: BabelCallee };\n}\n\n/**\n * Resolve the simple name of a call's callee, covering both a bare call\n * (`ensures(...)`) and a member call (`verify.ensures(...)` / `polly.ensures(...)`).\n * Computed member access (`obj[\"ensures\"](...)`) is intentionally not matched —\n * it cannot be resolved statically and is not a pattern polly emits.\n */\nfunction calleeName(callee: BabelCallee | undefined): string | undefined {\n if (!callee) return undefined;\n if (callee.type === \"Identifier\") {\n return (callee as BabelIdentifier).name;\n }\n if (callee.type === \"MemberExpression\") {\n const member = callee as BabelMemberExpression;\n if (!member.computed && member.property.type === \"Identifier\") {\n return member.property.name;\n }\n }\n return undefined;\n}\n\n/**\n * A Stryker `Ignore` plugin. Stryker calls `shouldIgnore` on entering each AST\n * node; returning a message marks that node — and every descendant, until the\n * node is left — as ignored. So matching the verify `CallExpression` itself\n * covers its condition and message arguments in one shot.\n */\nexport class PollyVerifyIgnorer implements Ignorer {\n constructor(private readonly primitives: ReadonlySet<string> = VERIFY_PRIMITIVES) {}\n\n shouldIgnore(path: NodePath): string | undefined {\n const callPath = path as CallExpressionPath;\n if (typeof callPath.isCallExpression !== \"function\" || !callPath.isCallExpression()) {\n return undefined;\n }\n const name = calleeName(callPath.node.callee);\n if (name && this.primitives.has(name)) {\n return (\n `Inside polly's ${name}(...) — a runtime no-op (compiled away in production, ` +\n `translated to a TLA+ assertion in verification). No test can observe or kill ` +\n `mutations here, so they are excluded from the score (polly#143).`\n );\n }\n return undefined;\n }\n}\n\n/** Reads `polly.excludeVerifyCallsites` (default: enabled) off Stryker options. */\nfunction isEnabled(options: StrykerOptions): boolean {\n const polly = (options as { polly?: { excludeVerifyCallsites?: boolean } }).polly;\n return polly?.excludeVerifyCallsites !== false;\n}\n\n// When disabled the plugin still loads but ignores nothing, so a shared config\n// can list it unconditionally and individual projects opt out via options.\nconst NOOP_IGNORER: Ignorer = { shouldIgnore: () => undefined };\n\nfunction pollyVerifyIgnorerFactory(options: StrykerOptions): Ignorer {\n return isEnabled(options) ? new PollyVerifyIgnorer() : NOOP_IGNORER;\n}\npollyVerifyIgnorerFactory.inject = tokens(commonTokens.options);\n\n/** The plugin array Stryker reads when this module is listed in `plugins`. */\nexport const strykerPlugins = [\n declareFactoryPlugin(PluginKind.Ignore, POLLY_VERIFY_IGNORER_NAME, pollyVerifyIgnorerFactory),\n];\n\n/**\n * A partial Stryker config that wires up the ignorer. Spread it into a\n * `stryker.conf.mjs` default export, or replicate its two keys in JSON.\n */\nexport const pollyStrykerPreset = {\n plugins: [\"@fairfox/polly/stryker\"],\n ignorers: [POLLY_VERIFY_IGNORER_NAME],\n} as const;\n\n// biome-ignore lint/style/noDefaultExport: ergonomic `import preset from \"@fairfox/polly/stryker\"`\nexport default pollyStrykerPreset;\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;AAkCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,IAAM,oBAAyC,IAAI,IAAI;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,IAAM,4BAA4B;AA0BzC,SAAS,UAAU,CAAC,QAAqD;AAAA,EACvE,IAAI,CAAC;AAAA,IAAQ;AAAA,EACb,IAAI,OAAO,SAAS,cAAc;AAAA,IAChC,OAAQ,OAA2B;AAAA,EACrC;AAAA,EACA,IAAI,OAAO,SAAS,oBAAoB;AAAA,IACtC,MAAM,SAAS;AAAA,IACf,IAAI,CAAC,OAAO,YAAY,OAAO,SAAS,SAAS,cAAc;AAAA,MAC7D,OAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AAAA,EACA;AAAA;AAAA;AASK,MAAM,mBAAsC;AAAA,EACpB;AAAA,EAA7B,WAAW,CAAkB,aAAkC,mBAAmB;AAAA,IAArD;AAAA;AAAA,EAE7B,YAAY,CAAC,MAAoC;AAAA,IAC/C,MAAM,WAAW;AAAA,IACjB,IAAI,OAAO,SAAS,qBAAqB,cAAc,CAAC,SAAS,iBAAiB,GAAG;AAAA,MACnF;AAAA,IACF;AAAA,IACA,MAAM,OAAO,WAAW,SAAS,KAAK,MAAM;AAAA,IAC5C,IAAI,QAAQ,KAAK,WAAW,IAAI,IAAI,GAAG;AAAA,MACrC,OACE,kBAAkB,+DAClB,kFACA;AAAA,IAEJ;AAAA,IACA;AAAA;AAEJ;AAGA,SAAS,SAAS,CAAC,SAAkC;AAAA,EACnD,MAAM,QAAS,QAA6D;AAAA,EAC5E,OAAO,OAAO,2BAA2B;AAAA;AAK3C,IAAM,eAAwB,EAAE,cAAc,MAAG;AAAA,EAAG;AAAA,EAAU;AAE9D,SAAS,yBAAyB,CAAC,SAAkC;AAAA,EACnE,OAAO,UAAU,OAAO,IAAI,IAAI,qBAAuB;AAAA;AAEzD,0BAA0B,SAAS,OAAO,aAAa,OAAO;AAGvD,IAAM,iBAAiB;AAAA,EAC5B,qBAAqB,WAAW,QAAQ,2BAA2B,yBAAyB;AAC9F;AAMO,IAAM,qBAAqB;AAAA,EAChC,SAAS,CAAC,wBAAwB;AAAA,EAClC,UAAU,CAAC,yBAAyB;AACtC;AAGA,IAAe;",
8
+ "debugId": "D1E2AADE2B7A729B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -1810,7 +1810,7 @@ class HandlerExtractor {
1810
1810
  const exists = handlers.some((h) => h.messageType === handler.messageType && h.location.file === handler.location.file);
1811
1811
  if (!exists) {
1812
1812
  handlers.push(handler);
1813
- if (this.isValidTLAIdentifier(handler.messageType)) {
1813
+ if (this.canRepresentAsTLAAction(handler.messageType)) {
1814
1814
  messageTypes.add(handler.messageType);
1815
1815
  } else {
1816
1816
  invalidMessageTypes.add(handler.messageType);
@@ -1920,7 +1920,7 @@ class HandlerExtractor {
1920
1920
  const syntheticHandlers = this.createResourceHandlers(resource, context);
1921
1921
  for (const handler of syntheticHandlers) {
1922
1922
  handlers.push(handler);
1923
- if (this.isValidTLAIdentifier(handler.messageType)) {
1923
+ if (this.canRepresentAsTLAAction(handler.messageType)) {
1924
1924
  messageTypes.add(handler.messageType);
1925
1925
  } else {
1926
1926
  invalidMessageTypes.add(handler.messageType);
@@ -1982,7 +1982,7 @@ class HandlerExtractor {
1982
1982
  }
1983
1983
  categorizeHandlerMessageTypes(handlers, messageTypes, invalidMessageTypes) {
1984
1984
  for (const handler of handlers) {
1985
- if (this.isValidTLAIdentifier(handler.messageType)) {
1985
+ if (this.canRepresentAsTLAAction(handler.messageType)) {
1986
1986
  messageTypes.add(handler.messageType);
1987
1987
  } else {
1988
1988
  invalidMessageTypes.add(handler.messageType);
@@ -1997,11 +1997,8 @@ class HandlerExtractor {
1997
1997
  console.log(`[DEBUG] Filtered ${invalidCount} invalid message type(s) from handlers`);
1998
1998
  }
1999
1999
  }
2000
- isValidTLAIdentifier(s) {
2001
- if (!s || s.length === 0) {
2002
- return false;
2003
- }
2004
- return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s);
2000
+ canRepresentAsTLAAction(s) {
2001
+ return typeof s === "string" && /[a-zA-Z]/.test(s) && !/[{}();<>=]/.test(s);
2005
2002
  }
2006
2003
  extractFromFile(sourceFile) {
2007
2004
  const handlers = [];
@@ -2684,8 +2681,20 @@ class HandlerExtractor {
2684
2681
  const paramName = this.extractPayloadPropertyParam(initializer);
2685
2682
  if (paramName !== null) {
2686
2683
  assignments.push({ field, value: `param:${paramName}` });
2684
+ return;
2685
+ }
2686
+ if (this.isTranslatableInitializer(initializer)) {
2687
+ assignments.push({ field, value: `EXPR:${initializer.getText()}` });
2688
+ return;
2689
+ }
2690
+ if (process.env["POLLY_DEBUG"]) {
2691
+ const on = signalName ? ` on ${signalName}.value` : "";
2692
+ console.log(`[WARN] dropped object-literal property '${name}'${on} — initializer kind ${initializer.getKindName()} is not translatable to TLA+`);
2687
2693
  }
2688
2694
  }
2695
+ isTranslatableInitializer(node) {
2696
+ return Node4.isBinaryExpression(node) || Node4.isPropertyAccessExpression(node) || Node4.isElementAccessExpression(node) || Node4.isPrefixUnaryExpression(node) || Node4.isPostfixUnaryExpression(node) || Node4.isParenthesizedExpression(node);
2697
+ }
2689
2698
  extractPayloadPropertyParam(initializer) {
2690
2699
  if (!Node4.isPropertyAccessExpression(initializer))
2691
2700
  return null;
@@ -6810,4 +6819,4 @@ main().catch((_error) => {
6810
6819
  process.exit(1);
6811
6820
  });
6812
6821
 
6813
- //# debugId=4CFC761842CDF97B64756E2164756E21
6822
+ //# debugId=1DBE75DC4C8D1D6864756E2164756E21