@fairfox/polly 0.12.3 → 0.13.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.
Files changed (36) hide show
  1. package/dist/src/background/index.js +118 -1
  2. package/dist/src/background/index.js.map +5 -4
  3. package/dist/src/background/message-router.js +118 -1
  4. package/dist/src/background/message-router.js.map +5 -4
  5. package/dist/src/index.d.ts +2 -0
  6. package/dist/src/index.js +203 -3
  7. package/dist/src/index.js.map +9 -6
  8. package/dist/src/shared/lib/constraints.d.ts +61 -0
  9. package/dist/src/shared/lib/context-helpers.js +118 -1
  10. package/dist/src/shared/lib/context-helpers.js.map +5 -4
  11. package/dist/src/shared/lib/message-bus.d.ts +22 -0
  12. package/dist/src/shared/lib/message-bus.js +118 -1
  13. package/dist/src/shared/lib/message-bus.js.map +5 -4
  14. package/dist/src/shared/lib/state.d.ts +8 -2
  15. package/dist/src/shared/lib/state.js +140 -3
  16. package/dist/src/shared/lib/state.js.map +6 -5
  17. package/dist/src/shared/lib/validation.d.ts +94 -0
  18. package/dist/src/shared/state/app-state.d.ts +4 -1
  19. package/dist/src/shared/state/app-state.js +140 -3
  20. package/dist/src/shared/state/app-state.js.map +6 -5
  21. package/dist/tools/analysis/src/extract/handlers.d.ts +24 -2
  22. package/dist/tools/teach/src/cli.js +102 -17
  23. package/dist/tools/teach/src/cli.js.map +3 -3
  24. package/dist/tools/teach/src/index.js +102 -17
  25. package/dist/tools/teach/src/index.js.map +3 -3
  26. package/dist/tools/verify/Dockerfile +11 -7
  27. package/dist/tools/verify/specs/Dockerfile +21 -5
  28. package/dist/tools/verify/specs/README.md +13 -6
  29. package/dist/tools/verify/src/cli.js +102 -17
  30. package/dist/tools/verify/src/cli.js.map +3 -3
  31. package/dist/tools/verify/src/config.js +8 -2
  32. package/dist/tools/verify/src/config.js.map +3 -3
  33. package/dist/tools/verify/src/primitives/index.d.ts +21 -8
  34. package/dist/tools/visualize/src/cli.js +102 -17
  35. package/dist/tools/visualize/src/cli.js.map +3 -3
  36. package/package.json +1 -1
@@ -49,7 +49,13 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
49
49
  });
50
50
 
51
51
  // tools/verify/src/primitives/index.ts
52
- function $constraints(_stateField, _constraints) {}
52
+ function $constraints(stateField, constraints, options) {
53
+ if (options?.runtime) {
54
+ import("../../../src/shared/lib/constraints.js").then(({ registerConstraints }) => {
55
+ registerConstraints(stateField, constraints);
56
+ }).catch(() => {});
57
+ }
58
+ }
53
59
 
54
60
  // tools/verify/src/config.ts
55
61
  function defineVerification(config) {
@@ -77,4 +83,4 @@ export {
77
83
  $constraints
78
84
  };
79
85
 
80
- //# debugId=06CC0A95E020BF9F64756E2164756E21
86
+ //# debugId=B7658306F59988EF64756E2164756E21
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
4
4
  "sourcesContent": [
5
- "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers.\n *\n * @example\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"loggedIn === true\", message: \"Must be logged in\" },\n * });\n */\nexport function $constraints(\n _stateField: string,\n _constraints: Record<\n string,\n {\n requires?: string;\n ensures?: string;\n message?: string;\n }\n >\n): void {\n // Runtime no-op - only used during verification\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n};\n",
5
+ "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n};\n",
6
6
  "// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export $constraints from primitives for user code\nexport { $constraints } from \"./primitives/index.js\";\n"
7
7
  ],
8
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwGO,SAAS,YAAY,CAC1B,aACA,cAQM;;;ACxBD,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
- "debugId": "06CC0A95E020BF9F64756E2164756E21",
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmHO,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;;;ACjDK,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
+ "debugId": "B7658306F59988EF64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -61,24 +61,37 @@ export declare function hasLength(array: unknown[], constraint: {
61
61
  max?: number;
62
62
  }): boolean;
63
63
  /**
64
- * Declare state-level constraints for verification.
64
+ * Declare state-level constraints for verification and optional runtime checking.
65
65
  * Maps message types to preconditions on state fields.
66
66
  *
67
- * The parser automatically wires these constraints to handlers.
67
+ * The parser automatically wires these constraints to handlers during verification.
68
+ * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.
68
69
  *
69
70
  * @example
71
+ * // Verification only (TLA+ generation)
70
72
  * const state = { loggedIn: false };
71
73
  *
72
74
  * $constraints("loggedIn", {
73
- * USER_LOGOUT: { requires: "loggedIn === true", message: "Must be logged in" },
74
- * BOOKMARK_ADD: { requires: "loggedIn === true", message: "Must be logged in" },
75
+ * USER_LOGOUT: { requires: "state.loggedIn === true", message: "Must be logged in" },
76
+ * BOOKMARK_ADD: { requires: "state.loggedIn === true", message: "Must be logged in" },
75
77
  * });
78
+ *
79
+ * @example
80
+ * // Runtime enforcement (function predicates)
81
+ * $constraints("loggedIn", {
82
+ * USER_LOGOUT: {
83
+ * requires: (state) => state.loggedIn === true,
84
+ * message: "Must be logged in to logout"
85
+ * },
86
+ * }, { runtime: true });
76
87
  */
77
- export declare function $constraints(_stateField: string, _constraints: Record<string, {
78
- requires?: string;
79
- ensures?: string;
88
+ export declare function $constraints(stateField: string, constraints: Record<string, {
89
+ requires?: string | ((state: unknown) => boolean);
90
+ ensures?: string | ((state: unknown) => boolean);
80
91
  message?: string;
81
- }>): void;
92
+ }>, options?: {
93
+ runtime?: boolean;
94
+ }): void;
82
95
  export declare const verify: {
83
96
  requires: typeof requires;
84
97
  ensures: typeof ensures;
@@ -1794,17 +1794,25 @@ class HandlerExtractor {
1794
1794
  const assignments = [];
1795
1795
  const preconditions = [];
1796
1796
  const postconditions = [];
1797
+ let actualHandler = null;
1797
1798
  if (Node4.isArrowFunction(handlerArg) || Node4.isFunctionExpression(handlerArg)) {
1798
- this.extractAssignments(handlerArg, assignments);
1799
- this.extractVerificationConditions(handlerArg, preconditions, postconditions);
1800
- this.checkAsyncMutations(handlerArg, messageType);
1799
+ actualHandler = handlerArg;
1800
+ } else if (Node4.isIdentifier(handlerArg)) {
1801
+ actualHandler = this.resolveFunctionReference(handlerArg);
1802
+ }
1803
+ if (actualHandler) {
1804
+ this.extractAssignments(actualHandler, assignments);
1805
+ this.extractVerificationConditions(actualHandler, preconditions, postconditions);
1806
+ if (Node4.isArrowFunction(actualHandler) || Node4.isFunctionExpression(actualHandler)) {
1807
+ this.checkAsyncMutations(actualHandler, messageType);
1808
+ }
1801
1809
  }
1802
1810
  const line = callExpr.getStartLineNumber();
1803
1811
  const sourceFile = callExpr.getSourceFile();
1804
1812
  const handlerName = `${messageType}_handler`;
1805
1813
  let relationships;
1806
- if (Node4.isArrowFunction(handlerArg) || Node4.isFunctionExpression(handlerArg)) {
1807
- const detectedRelationships = this.relationshipExtractor.extractFromHandler(handlerArg, sourceFile, handlerName);
1814
+ if (actualHandler) {
1815
+ const detectedRelationships = this.relationshipExtractor.extractFromHandler(actualHandler, sourceFile, handlerName);
1808
1816
  if (detectedRelationships.length > 0) {
1809
1817
  relationships = detectedRelationships;
1810
1818
  }
@@ -1830,6 +1838,9 @@ class HandlerExtractor {
1830
1838
  if (Node4.isCallExpression(node)) {
1831
1839
  this.extractArrayMutationAssignment(node, assignments);
1832
1840
  }
1841
+ if (Node4.isPostfixUnaryExpression(node) || Node4.isPrefixUnaryExpression(node)) {
1842
+ this.extractUnaryExpressionAssignment(node, assignments);
1843
+ }
1833
1844
  });
1834
1845
  }
1835
1846
  extractBinaryExpressionAssignment(node, assignments) {
@@ -1905,6 +1916,36 @@ class HandlerExtractor {
1905
1916
  }
1906
1917
  }
1907
1918
  }
1919
+ getUnaryOperatorText(operator) {
1920
+ if (typeof operator === "number") {
1921
+ if (operator === SyntaxKind.PlusPlusToken)
1922
+ return "++";
1923
+ if (operator === SyntaxKind.MinusMinusToken)
1924
+ return "--";
1925
+ return null;
1926
+ }
1927
+ if (operator && typeof operator === "object" && "getText" in operator) {
1928
+ return operator.getText();
1929
+ }
1930
+ return null;
1931
+ }
1932
+ extractUnaryExpressionAssignment(node, assignments) {
1933
+ if (!Node4.isPostfixUnaryExpression(node) && !Node4.isPrefixUnaryExpression(node))
1934
+ return;
1935
+ const operator = node.getOperatorToken();
1936
+ const operatorText = this.getUnaryOperatorText(operator);
1937
+ if (operatorText !== "++" && operatorText !== "--")
1938
+ return;
1939
+ const operand = node.getOperand();
1940
+ if (!Node4.isPropertyAccessExpression(operand))
1941
+ return;
1942
+ const fieldPath = this.getPropertyPath(operand);
1943
+ if (fieldPath.startsWith("state.")) {
1944
+ const field = fieldPath.substring(6);
1945
+ const value = operatorText === "++" ? "@ + 1" : "@ - 1";
1946
+ assignments.push({ field, value });
1947
+ }
1948
+ }
1908
1949
  extractArrayMutationAssignment(node, assignments) {
1909
1950
  if (!Node4.isCallExpression(node))
1910
1951
  return;
@@ -1984,7 +2025,7 @@ class HandlerExtractor {
1984
2025
  }
1985
2026
  extractVerificationConditions(funcNode, preconditions, postconditions) {
1986
2027
  const body = funcNode.getBody();
1987
- const statements = Node4.isBlock(body) ? body.getStatements() : [body];
2028
+ const statements = Node4.isBlock(body) ? body.getStatements() : body ? [body] : [];
1988
2029
  for (const statement of statements) {
1989
2030
  this.processStatementForConditions(statement, preconditions, postconditions);
1990
2031
  }
@@ -2104,10 +2145,14 @@ class HandlerExtractor {
2104
2145
  const handlers = [];
2105
2146
  try {
2106
2147
  const initializer = varDecl.getInitializer();
2107
- if (!this.isHandlerMapInitializer(initializer, varDecl)) {
2148
+ if (!initializer) {
2108
2149
  return handlers;
2109
2150
  }
2110
- const properties = initializer.getProperties();
2151
+ const objectLiteral = this.getHandlerMapObject(initializer, varDecl);
2152
+ if (!objectLiteral) {
2153
+ return handlers;
2154
+ }
2155
+ const properties = objectLiteral.getProperties();
2111
2156
  for (const prop of properties) {
2112
2157
  const handler = this.extractHandlerFromProperty(prop, context, filePath);
2113
2158
  if (handler) {
@@ -2117,12 +2162,16 @@ class HandlerExtractor {
2117
2162
  } catch (_error) {}
2118
2163
  return handlers;
2119
2164
  }
2120
- isHandlerMapInitializer(initializer, varDecl) {
2121
- if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
2122
- return false;
2165
+ getHandlerMapObject(initializer, varDecl) {
2166
+ if (Node4.isObjectLiteralExpression(initializer)) {
2167
+ if (this.isHandlerMapName(varDecl.getName())) {
2168
+ return initializer;
2169
+ }
2123
2170
  }
2124
- const varName = varDecl.getName().toLowerCase();
2125
- return /(handler|listener|callback|event)s?/.test(varName);
2171
+ return null;
2172
+ }
2173
+ isHandlerMapName(varName) {
2174
+ return /(handler|listener|callback|event)s?/.test(varName.toLowerCase());
2126
2175
  }
2127
2176
  extractHandlerFromProperty(prop, context, filePath) {
2128
2177
  if (!Node4.isPropertyAssignment(prop)) {
@@ -2133,13 +2182,29 @@ class HandlerExtractor {
2133
2182
  if (!messageType) {
2134
2183
  return null;
2135
2184
  }
2185
+ const value = prop.getInitializer();
2186
+ const assignments = [];
2187
+ const preconditions = [];
2188
+ const postconditions = [];
2189
+ if (value) {
2190
+ if (Node4.isArrowFunction(value) || Node4.isFunctionExpression(value)) {
2191
+ this.extractAssignments(value, assignments);
2192
+ this.extractVerificationConditions(value, preconditions, postconditions);
2193
+ } else if (Node4.isIdentifier(value)) {
2194
+ const referencedFunction = this.resolveFunctionReference(value);
2195
+ if (referencedFunction) {
2196
+ this.extractAssignments(referencedFunction, assignments);
2197
+ this.extractVerificationConditions(referencedFunction, preconditions, postconditions);
2198
+ }
2199
+ }
2200
+ }
2136
2201
  const line = prop.getStartLineNumber();
2137
2202
  return {
2138
2203
  messageType,
2139
2204
  node: context,
2140
- assignments: [],
2141
- preconditions: [],
2142
- postconditions: [],
2205
+ assignments,
2206
+ preconditions,
2207
+ postconditions,
2143
2208
  location: { file: filePath, line }
2144
2209
  };
2145
2210
  }
@@ -2152,6 +2217,26 @@ class HandlerExtractor {
2152
2217
  }
2153
2218
  return null;
2154
2219
  }
2220
+ resolveFunctionReference(identifier) {
2221
+ if (!Node4.isIdentifier(identifier)) {
2222
+ return null;
2223
+ }
2224
+ try {
2225
+ const definitions = identifier.getDefinitionNodes();
2226
+ for (const def of definitions) {
2227
+ if (Node4.isFunctionDeclaration(def)) {
2228
+ return def;
2229
+ }
2230
+ if (Node4.isVariableDeclaration(def)) {
2231
+ const initializer = def.getInitializer();
2232
+ if (initializer && (Node4.isArrowFunction(initializer) || Node4.isFunctionExpression(initializer))) {
2233
+ return initializer;
2234
+ }
2235
+ }
2236
+ }
2237
+ } catch (_error) {}
2238
+ return null;
2239
+ }
2155
2240
  extractTypeGuardHandlers(ifNode, context, filePath) {
2156
2241
  const handlers = [];
2157
2242
  try {
@@ -4934,4 +5019,4 @@ main().catch((_error) => {
4934
5019
  process.exit(1);
4935
5020
  });
4936
5021
 
4937
- //# debugId=17D3ACE789AF647764756E2164756E21
5022
+ //# debugId=29B7FAEF737837BA64756E2164756E21